summaryrefslogtreecommitdiff
path: root/onionshare
diff options
context:
space:
mode:
Diffstat (limited to 'onionshare')
-rw-r--r--onionshare/__init__.py247
-rw-r--r--onionshare/common.py256
-rw-r--r--onionshare/onion.py442
-rw-r--r--onionshare/onionshare.py20
-rw-r--r--onionshare/settings.py162
-rw-r--r--onionshare/strings.py9
-rw-r--r--onionshare/web/receive_mode.py365
-rw-r--r--onionshare/web/send_base_mode.py305
-rw-r--r--onionshare/web/share_mode.py324
-rw-r--r--onionshare/web/web.py343
-rw-r--r--onionshare/web/website_mode.py104
11 files changed, 1749 insertions, 828 deletions
diff --git a/onionshare/__init__.py b/onionshare/__init__.py
index 620ada98..108ffd0b 100644
--- a/onionshare/__init__.py
+++ b/onionshare/__init__.py
@@ -27,6 +27,15 @@ from .web import Web
from .onion import *
from .onionshare import OnionShare
+
+def build_url(common, app, web):
+ # Build the URL
+ if common.settings.get("public_mode"):
+ return "http://{0:s}".format(app.onion_host)
+ else:
+ return "http://onionshare:{0:s}@{1:s}".format(web.password, app.onion_host)
+
+
def main(cwd=None):
"""
The main() function implements all of the logic that the command-line version of
@@ -38,22 +47,84 @@ def main(cwd=None):
print("OnionShare {0:s} | https://onionshare.org/".format(common.version))
# OnionShare CLI in OSX needs to change current working directory (#132)
- if common.platform == 'Darwin':
+ if common.platform == "Darwin":
if cwd:
os.chdir(cwd)
# Parse arguments
- parser = argparse.ArgumentParser(formatter_class=lambda prog: argparse.HelpFormatter(prog,max_help_position=28))
- parser.add_argument('--local-only', action='store_true', dest='local_only', help="Don't use Tor (only for development)")
- parser.add_argument('--stay-open', action='store_true', dest='stay_open', help="Continue sharing after files have been sent")
- parser.add_argument('--auto-start-timer', metavar='<int>', dest='autostart_timer', default=0, help="Schedule this share to start N seconds from now")
- parser.add_argument('--auto-stop-timer', metavar='<int>', dest='autostop_timer', default=0, help="Stop sharing after a given amount of seconds")
- parser.add_argument('--connect-timeout', metavar='<int>', dest='connect_timeout', default=120, help="Give up connecting to Tor after a given amount of seconds (default: 120)")
- parser.add_argument('--stealth', action='store_true', dest='stealth', help="Use client authorization (advanced)")
- parser.add_argument('--receive', action='store_true', dest='receive', help="Receive shares instead of sending them")
- parser.add_argument('--config', metavar='config', default=False, help="Custom JSON config file location (optional)")
- parser.add_argument('-v', '--verbose', action='store_true', dest='verbose', help="Log OnionShare errors to stdout, and web errors to disk")
- parser.add_argument('filename', metavar='filename', nargs='*', help="List of files or folders to share")
+ parser = argparse.ArgumentParser(
+ formatter_class=lambda prog: argparse.HelpFormatter(prog, max_help_position=28)
+ )
+ parser.add_argument(
+ "--local-only",
+ action="store_true",
+ dest="local_only",
+ help="Don't use Tor (only for development)",
+ )
+ parser.add_argument(
+ "--stay-open",
+ action="store_true",
+ dest="stay_open",
+ help="Continue sharing after files have been sent",
+ )
+ parser.add_argument(
+ "--auto-start-timer",
+ metavar="<int>",
+ dest="autostart_timer",
+ default=0,
+ help="Schedule this share to start N seconds from now",
+ )
+ parser.add_argument(
+ "--auto-stop-timer",
+ metavar="<int>",
+ dest="autostop_timer",
+ default=0,
+ help="Stop sharing after a given amount of seconds",
+ )
+ parser.add_argument(
+ "--connect-timeout",
+ metavar="<int>",
+ dest="connect_timeout",
+ default=120,
+ help="Give up connecting to Tor after a given amount of seconds (default: 120)",
+ )
+ parser.add_argument(
+ "--stealth",
+ action="store_true",
+ dest="stealth",
+ help="Use client authorization (advanced)",
+ )
+ parser.add_argument(
+ "--receive",
+ action="store_true",
+ dest="receive",
+ help="Receive shares instead of sending them",
+ )
+ parser.add_argument(
+ "--website",
+ action="store_true",
+ dest="website",
+ help="Publish a static website",
+ )
+ parser.add_argument(
+ "--config",
+ metavar="config",
+ default=False,
+ help="Custom JSON config file location (optional)",
+ )
+ parser.add_argument(
+ "-v",
+ "--verbose",
+ action="store_true",
+ dest="verbose",
+ help="Log OnionShare errors to stdout, and web errors to disk",
+ )
+ parser.add_argument(
+ "filename",
+ metavar="filename",
+ nargs="*",
+ help="List of files or folders to share",
+ )
args = parser.parse_args()
filenames = args.filename
@@ -68,20 +139,24 @@ def main(cwd=None):
connect_timeout = int(args.connect_timeout)
stealth = bool(args.stealth)
receive = bool(args.receive)
+ website = bool(args.website)
config = args.config
if receive:
- mode = 'receive'
+ mode = "receive"
+ elif website:
+ mode = "website"
else:
- mode = 'share'
+ mode = "share"
- # Make sure filenames given if not using receiver mode
- if mode == 'share' and len(filenames) == 0:
- parser.print_help()
- sys.exit()
+ # In share an website mode, you must supply a list of filenames
+ if mode == "share" or mode == "website":
+ # Make sure filenames given if not using receiver mode
+ if len(filenames) == 0:
+ parser.print_help()
+ sys.exit()
- # Validate filenames
- if mode == 'share':
+ # Validate filenames
valid = True
for filename in filenames:
if not os.path.isfile(filename) and not os.path.isdir(filename):
@@ -96,6 +171,8 @@ def main(cwd=None):
# Re-load settings, if a custom config was passed in
if config:
common.load_settings(config)
+ else:
+ common.load_settings()
# Verbose mode?
common.verbose = verbose
@@ -106,7 +183,9 @@ def main(cwd=None):
# Start the Onion object
onion = Onion(common)
try:
- onion.connect(custom_settings=False, config=config, connect_timeout=connect_timeout)
+ onion.connect(
+ custom_settings=False, config=config, connect_timeout=connect_timeout
+ )
except KeyboardInterrupt:
print("")
sys.exit()
@@ -116,44 +195,66 @@ def main(cwd=None):
# Start the onionshare app
try:
common.settings.load()
- if not common.settings.get('public_mode'):
- web.generate_slug(common.settings.get('slug'))
+ if not common.settings.get("public_mode"):
+ web.generate_password(common.settings.get("password"))
else:
- web.slug = None
+ web.password = None
app = OnionShare(common, onion, local_only, autostop_timer)
app.set_stealth(stealth)
app.choose_port()
+
# Delay the startup if a startup timer was set
if autostart_timer > 0:
# Can't set a schedule that is later than the auto-stop timer
if app.autostop_timer > 0 and app.autostop_timer < autostart_timer:
- print("The auto-stop time can't be the same or earlier than the auto-start time. Please update it to start sharing.")
+ print(
+ "The auto-stop time can't be the same or earlier than the auto-start time. Please update it to start sharing."
+ )
sys.exit()
app.start_onion_service(False, True)
- if common.settings.get('public_mode'):
- url = 'http://{0:s}'.format(app.onion_host)
- else:
- url = 'http://{0:s}/{1:s}'.format(app.onion_host, web.slug)
+ url = build_url(common, app, web)
schedule = datetime.now() + timedelta(seconds=autostart_timer)
- if mode == 'receive':
- print("Files sent to you appear in this folder: {}".format(common.settings.get('data_dir')))
- print('')
- print("Warning: Receive mode lets people upload files to your computer. Some files can potentially take control of your computer if you open them. Only open things from people you trust, or if you know what you are doing.")
- print('')
+ if mode == "receive":
+ print(
+ "Files sent to you appear in this folder: {}".format(
+ common.settings.get("data_dir")
+ )
+ )
+ print("")
+ print(
+ "Warning: Receive mode lets people upload files to your computer. Some files can potentially take control of your computer if you open them. Only open things from people you trust, or if you know what you are doing."
+ )
+ print("")
if stealth:
- print("Give this address and HidServAuth lineto your sender, and tell them it won't be accessible until: {}".format(schedule.strftime("%I:%M:%S%p, %b %d, %y")))
+ print(
+ "Give this address and HidServAuth lineto your sender, and tell them it won't be accessible until: {}".format(
+ schedule.strftime("%I:%M:%S%p, %b %d, %y")
+ )
+ )
print(app.auth_string)
else:
- print("Give this address to your sender, and tell them it won't be accessible until: {}".format(schedule.strftime("%I:%M:%S%p, %b %d, %y")))
+ print(
+ "Give this address to your sender, and tell them it won't be accessible until: {}".format(
+ schedule.strftime("%I:%M:%S%p, %b %d, %y")
+ )
+ )
else:
if stealth:
- print("Give this address and HidServAuth line to your recipient, and tell them it won't be accessible until: {}".format(schedule.strftime("%I:%M:%S%p, %b %d, %y")))
+ print(
+ "Give this address and HidServAuth line to your recipient, and tell them it won't be accessible until: {}".format(
+ schedule.strftime("%I:%M:%S%p, %b %d, %y")
+ )
+ )
print(app.auth_string)
else:
- print("Give this address to your recipient, and tell them it won't be accessible until: {}".format(schedule.strftime("%I:%M:%S%p, %b %d, %y")))
+ print(
+ "Give this address to your recipient, and tell them it won't be accessible until: {}".format(
+ schedule.strftime("%I:%M:%S%p, %b %d, %y")
+ )
+ )
print(url)
- print('')
+ print("")
print("Waiting for the scheduled time before starting...")
app.onion.cleanup(False)
time.sleep(autostart_timer)
@@ -168,7 +269,15 @@ def main(cwd=None):
print(e.args[0])
sys.exit()
- if mode == 'share':
+ if mode == "website":
+ # Prepare files to share
+ try:
+ web.website_mode.set_file_info(filenames)
+ except OSError as e:
+ print(e.strerror)
+ sys.exit(1)
+
+ if mode == "share":
# Prepare files to share
print("Compressing files.")
try:
@@ -180,44 +289,50 @@ def main(cwd=None):
# Warn about sending large files over Tor
if web.share_mode.download_filesize >= 157286400: # 150mb
- print('')
+ print("")
print("Warning: Sending a large share could take hours")
- print('')
+ print("")
# Start OnionShare http service in new thread
- t = threading.Thread(target=web.start, args=(app.port, stay_open, common.settings.get('public_mode'), web.slug))
+ t = threading.Thread(
+ target=web.start,
+ args=(app.port, stay_open, common.settings.get("public_mode"), web.password),
+ )
t.daemon = True
t.start()
try: # Trap Ctrl-C
- # Wait for web.generate_slug() to finish running
+ # Wait for web.generate_password() to finish running
time.sleep(0.2)
# start auto-stop timer thread
if app.autostop_timer > 0:
app.autostop_timer_thread.start()
- # Save the web slug if we are using a persistent private key
- if common.settings.get('save_private_key'):
- if not common.settings.get('slug'):
- common.settings.set('slug', web.slug)
+ # Save the web password if we are using a persistent private key
+ if common.settings.get("save_private_key"):
+ if not common.settings.get("password"):
+ common.settings.set("password", web.password)
common.settings.save()
# Build the URL
- if common.settings.get('public_mode'):
- url = 'http://{0:s}'.format(app.onion_host)
- else:
- url = 'http://{0:s}/{1:s}'.format(app.onion_host, web.slug)
+ url = build_url(common, app, web)
- print('')
+ print("")
if autostart_timer > 0:
print("Server started")
else:
- if mode == 'receive':
- print("Files sent to you appear in this folder: {}".format(common.settings.get('data_dir')))
- print('')
- print("Warning: Receive mode lets people upload files to your computer. Some files can potentially take control of your computer if you open them. Only open things from people you trust, or if you know what you are doing.")
- print('')
+ if mode == "receive":
+ print(
+ "Files sent to you appear in this folder: {}".format(
+ common.settings.get("data_dir")
+ )
+ )
+ print("")
+ print(
+ "Warning: Receive mode lets people upload files to your computer. Some files can potentially take control of your computer if you open them. Only open things from people you trust, or if you know what you are doing."
+ )
+ print("")
if stealth:
print("Give this address and HidServAuth to the sender:")
@@ -234,7 +349,7 @@ def main(cwd=None):
else:
print("Give this address to the recipient:")
print(url)
- print('')
+ print("")
print("Press Ctrl+C to stop the server")
# Wait for app to close
@@ -242,14 +357,17 @@ def main(cwd=None):
if app.autostop_timer > 0:
# if the auto-stop timer was set and has run out, stop the server
if not app.autostop_timer_thread.is_alive():
- if mode == 'share':
+ if mode == "share" or (mode == "website"):
# If there were no attempts to download the share, or all downloads are done, we can stop
- if web.share_mode.download_count == 0 or web.done:
+ if web.share_mode.cur_history_id == 0 or web.done:
print("Stopped because auto-stop timer ran out")
web.stop(app.port)
break
- if mode == 'receive':
- if web.receive_mode.upload_count == 0 or not web.receive_mode.uploads_in_progress:
+ if mode == "receive":
+ if (
+ web.receive_mode.cur_history_id == 0
+ or not web.receive_mode.uploads_in_progress
+ ):
print("Stopped because auto-stop timer ran out")
web.stop(app.port)
break
@@ -265,5 +383,6 @@ def main(cwd=None):
app.cleanup()
onion.cleanup()
-if __name__ == '__main__':
+
+if __name__ == "__main__":
main()
diff --git a/onionshare/common.py b/onionshare/common.py
index 325f11d4..3373462b 100644
--- a/onionshare/common.py
+++ b/onionshare/common.py
@@ -36,16 +36,17 @@ class Common(object):
"""
The Common object is shared amongst all parts of OnionShare.
"""
+
def __init__(self, verbose=False):
self.verbose = verbose
# The platform OnionShare is running on
self.platform = platform.system()
- if self.platform.endswith('BSD'):
- self.platform = 'BSD'
+ if self.platform.endswith("BSD") or self.platform == "DragonFly":
+ self.platform = "BSD"
# The current version of OnionShare
- with open(self.get_resource_path('version.txt')) as f:
+ with open(self.get_resource_path("version.txt")) as f:
self.version = f.read().strip()
def load_settings(self, config=None):
@@ -64,7 +65,7 @@ class Common(object):
final_msg = "[{}] {}.{}".format(timestamp, module, func)
if msg:
- final_msg = '{}: {}'.format(final_msg, msg)
+ final_msg = "{}: {}".format(final_msg, msg)
print(final_msg)
def get_resource_path(self, filename):
@@ -73,85 +74,118 @@ class Common(object):
systemwide, and whether regardless of platform
"""
# On Windows, and in Windows dev mode, switch slashes in incoming filename to backslackes
- if self.platform == 'Windows':
- filename = filename.replace('/', '\\')
+ if self.platform == "Windows":
+ filename = filename.replace("/", "\\")
- if getattr(sys, 'onionshare_dev_mode', False):
+ if getattr(sys, "onionshare_dev_mode", False):
# Look for resources directory relative to python file
- prefix = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe())))), 'share')
+ prefix = os.path.join(
+ os.path.dirname(
+ os.path.dirname(
+ os.path.abspath(inspect.getfile(inspect.currentframe()))
+ )
+ ),
+ "share",
+ )
if not os.path.exists(prefix):
# While running tests during stdeb bdist_deb, look 3 directories up for the share folder
- prefix = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(prefix)))), 'share')
-
- elif self.platform == 'BSD' or self.platform == 'Linux':
+ prefix = os.path.join(
+ os.path.dirname(
+ os.path.dirname(os.path.dirname(os.path.dirname(prefix)))
+ ),
+ "share",
+ )
+
+ elif self.platform == "BSD" or self.platform == "Linux":
# Assume OnionShare is installed systemwide in Linux, since we're not running in dev mode
- prefix = os.path.join(sys.prefix, 'share/onionshare')
+ prefix = os.path.join(sys.prefix, "share/onionshare")
- elif getattr(sys, 'frozen', False):
+ elif getattr(sys, "frozen", False):
# Check if app is "frozen"
# https://pythonhosted.org/PyInstaller/#run-time-information
- if self.platform == 'Darwin':
- prefix = os.path.join(sys._MEIPASS, 'share')
- elif self.platform == 'Windows':
- prefix = os.path.join(os.path.dirname(sys.executable), 'share')
+ if self.platform == "Darwin":
+ prefix = os.path.join(sys._MEIPASS, "share")
+ elif self.platform == "Windows":
+ prefix = os.path.join(os.path.dirname(sys.executable), "share")
return os.path.join(prefix, filename)
def get_tor_paths(self):
- if self.platform == 'Linux':
- tor_path = '/usr/bin/tor'
- tor_geo_ip_file_path = '/usr/share/tor/geoip'
- tor_geo_ipv6_file_path = '/usr/share/tor/geoip6'
- obfs4proxy_file_path = '/usr/bin/obfs4proxy'
- elif self.platform == 'Windows':
- base_path = os.path.join(os.path.dirname(os.path.dirname(self.get_resource_path(''))), 'tor')
- tor_path = os.path.join(os.path.join(base_path, 'Tor'), 'tor.exe')
- obfs4proxy_file_path = os.path.join(os.path.join(base_path, 'Tor'), 'obfs4proxy.exe')
- tor_geo_ip_file_path = os.path.join(os.path.join(os.path.join(base_path, 'Data'), 'Tor'), 'geoip')
- tor_geo_ipv6_file_path = os.path.join(os.path.join(os.path.join(base_path, 'Data'), 'Tor'), 'geoip6')
- elif self.platform == 'Darwin':
- base_path = os.path.dirname(os.path.dirname(os.path.dirname(self.get_resource_path(''))))
- tor_path = os.path.join(base_path, 'Resources', 'Tor', 'tor')
- tor_geo_ip_file_path = os.path.join(base_path, 'Resources', 'Tor', 'geoip')
- tor_geo_ipv6_file_path = os.path.join(base_path, 'Resources', 'Tor', 'geoip6')
- obfs4proxy_file_path = os.path.join(base_path, 'Resources', 'Tor', 'obfs4proxy')
- elif self.platform == 'BSD':
- tor_path = '/usr/local/bin/tor'
- tor_geo_ip_file_path = '/usr/local/share/tor/geoip'
- tor_geo_ipv6_file_path = '/usr/local/share/tor/geoip6'
- obfs4proxy_file_path = '/usr/local/bin/obfs4proxy'
-
- return (tor_path, tor_geo_ip_file_path, tor_geo_ipv6_file_path, obfs4proxy_file_path)
+ if self.platform == "Linux":
+ tor_path = "/usr/bin/tor"
+ tor_geo_ip_file_path = "/usr/share/tor/geoip"
+ tor_geo_ipv6_file_path = "/usr/share/tor/geoip6"
+ obfs4proxy_file_path = "/usr/bin/obfs4proxy"
+ elif self.platform == "Windows":
+ base_path = os.path.join(
+ os.path.dirname(os.path.dirname(self.get_resource_path(""))), "tor"
+ )
+ tor_path = os.path.join(os.path.join(base_path, "Tor"), "tor.exe")
+ obfs4proxy_file_path = os.path.join(
+ os.path.join(base_path, "Tor"), "obfs4proxy.exe"
+ )
+ tor_geo_ip_file_path = os.path.join(
+ os.path.join(os.path.join(base_path, "Data"), "Tor"), "geoip"
+ )
+ tor_geo_ipv6_file_path = os.path.join(
+ os.path.join(os.path.join(base_path, "Data"), "Tor"), "geoip6"
+ )
+ elif self.platform == "Darwin":
+ base_path = os.path.dirname(
+ os.path.dirname(os.path.dirname(self.get_resource_path("")))
+ )
+ tor_path = os.path.join(base_path, "Resources", "Tor", "tor")
+ tor_geo_ip_file_path = os.path.join(base_path, "Resources", "Tor", "geoip")
+ tor_geo_ipv6_file_path = os.path.join(
+ base_path, "Resources", "Tor", "geoip6"
+ )
+ obfs4proxy_file_path = os.path.join(
+ base_path, "Resources", "Tor", "obfs4proxy"
+ )
+ elif self.platform == "BSD":
+ tor_path = "/usr/local/bin/tor"
+ tor_geo_ip_file_path = "/usr/local/share/tor/geoip"
+ tor_geo_ipv6_file_path = "/usr/local/share/tor/geoip6"
+ obfs4proxy_file_path = "/usr/local/bin/obfs4proxy"
+
+ return (
+ tor_path,
+ tor_geo_ip_file_path,
+ tor_geo_ipv6_file_path,
+ obfs4proxy_file_path,
+ )
def build_data_dir(self):
"""
Returns the path of the OnionShare data directory.
"""
- if self.platform == 'Windows':
+ if self.platform == "Windows":
try:
- appdata = os.environ['APPDATA']
- onionshare_data_dir = '{}\\OnionShare'.format(appdata)
+ appdata = os.environ["APPDATA"]
+ onionshare_data_dir = "{}\\OnionShare".format(appdata)
except:
# If for some reason we don't have the 'APPDATA' environment variable
# (like running tests in Linux while pretending to be in Windows)
- onionshare_data_dir = os.path.expanduser('~/.config/onionshare')
- elif self.platform == 'Darwin':
- onionshare_data_dir = os.path.expanduser('~/Library/Application Support/OnionShare')
+ onionshare_data_dir = os.path.expanduser("~/.config/onionshare")
+ elif self.platform == "Darwin":
+ onionshare_data_dir = os.path.expanduser(
+ "~/Library/Application Support/OnionShare"
+ )
else:
- onionshare_data_dir = os.path.expanduser('~/.config/onionshare')
+ onionshare_data_dir = os.path.expanduser("~/.config/onionshare")
os.makedirs(onionshare_data_dir, 0o700, True)
return onionshare_data_dir
- def build_slug(self):
+ def build_password(self):
"""
Returns a random string made from two words from the wordlist, such as "deter-trig".
"""
- with open(self.get_resource_path('wordlist.txt')) as f:
+ with open(self.get_resource_path("wordlist.txt")) as f:
wordlist = f.read().split()
r = random.SystemRandom()
- return '-'.join(r.choice(wordlist) for _ in range(2))
+ return "-".join(r.choice(wordlist) for _ in range(2))
def define_css(self):
"""
@@ -160,7 +194,7 @@ class Common(object):
"""
self.css = {
# OnionShareGui styles
- 'mode_switcher_selected_style': """
+ "mode_switcher_selected_style": """
QPushButton {
color: #ffffff;
background-color: #4e064f;
@@ -169,8 +203,7 @@ class Common(object):
font-weight: bold;
border-radius: 0;
}""",
-
- 'mode_switcher_unselected_style': """
+ "mode_switcher_unselected_style": """
QPushButton {
color: #ffffff;
background-color: #601f61;
@@ -178,23 +211,20 @@ class Common(object):
font-weight: normal;
border-radius: 0;
}""",
-
- 'settings_button': """
+ "settings_button": """
QPushButton {
background-color: #601f61;
border: 0;
border-left: 1px solid #69266b;
border-radius: 0;
}""",
-
- 'server_status_indicator_label': """
+ "server_status_indicator_label": """
QLabel {
font-style: italic;
color: #666666;
padding: 2px;
}""",
-
- 'status_bar': """
+ "status_bar": """
QStatusBar {
font-style: italic;
color: #666666;
@@ -202,16 +232,14 @@ class Common(object):
QStatusBar::item {
border: 0px;
}""",
-
- # Common styles between ShareMode and ReceiveMode and their child widgets
- 'mode_info_label': """
+ # Common styles between modes and their child widgets
+ "mode_info_label": """
QLabel {
font-size: 12px;
color: #666666;
}
""",
-
- 'server_status_url': """
+ "server_status_url": """
QLabel {
background-color: #ffffff;
color: #000000;
@@ -220,14 +248,12 @@ class Common(object):
font-size: 12px;
}
""",
-
- 'server_status_url_buttons': """
+ "server_status_url_buttons": """
QPushButton {
color: #3f7fcf;
}
""",
-
- 'server_status_button_stopped': """
+ "server_status_button_stopped": """
QPushButton {
background-color: #5fa416;
color: #ffffff;
@@ -235,8 +261,7 @@ class Common(object):
border: 0;
border-radius: 5px;
}""",
-
- 'server_status_button_working': """
+ "server_status_button_working": """
QPushButton {
background-color: #4c8211;
color: #ffffff;
@@ -245,8 +270,7 @@ class Common(object):
border-radius: 5px;
font-style: italic;
}""",
-
- 'server_status_button_started': """
+ "server_status_button_started": """
QPushButton {
background-color: #d0011b;
color: #ffffff;
@@ -254,8 +278,7 @@ class Common(object):
border: 0;
border-radius: 5px;
}""",
-
- 'downloads_uploads_empty': """
+ "downloads_uploads_empty": """
QWidget {
background-color: #ffffff;
border: 1px solid #999999;
@@ -265,13 +288,11 @@ class Common(object):
border: 0px;
}
""",
-
- 'downloads_uploads_empty_text': """
+ "downloads_uploads_empty_text": """
QLabel {
color: #999999;
}""",
-
- 'downloads_uploads_label': """
+ "downloads_uploads_label": """
QLabel {
font-weight: bold;
font-size 14px;
@@ -279,14 +300,12 @@ class Common(object):
background-color: none;
border: none;
}""",
-
- 'downloads_uploads_clear': """
+ "downloads_uploads_clear": """
QPushButton {
color: #3f7fcf;
}
""",
-
- 'download_uploads_indicator': """
+ "download_uploads_indicator": """
QLabel {
color: #ffffff;
background-color: #f44449;
@@ -296,8 +315,7 @@ class Common(object):
border-radius: 7px;
text-align: center;
}""",
-
- 'downloads_uploads_progress_bar': """
+ "downloads_uploads_progress_bar": """
QProgressBar {
border: 1px solid #4e064f;
background-color: #ffffff !important;
@@ -309,9 +327,20 @@ class Common(object):
background-color: #4e064f;
width: 10px;
}""",
-
+ "history_individual_file_timestamp_label": """
+ QLabel {
+ color: #666666;
+ }""",
+ "history_individual_file_status_code_label_2xx": """
+ QLabel {
+ color: #008800;
+ }""",
+ "history_individual_file_status_code_label_4xx": """
+ QLabel {
+ color: #cc0000;
+ }""",
# Share mode and child widget styles
- 'share_zip_progess_bar': """
+ "share_zip_progess_bar": """
QProgressBar {
border: 1px solid #4e064f;
background-color: #ffffff !important;
@@ -323,21 +352,18 @@ class Common(object):
background-color: #4e064f;
width: 10px;
}""",
-
- 'share_filesize_warning': """
+ "share_filesize_warning": """
QLabel {
padding: 10px 0;
font-weight: bold;
color: #333333;
}
""",
-
- 'share_file_selection_drop_here_label': """
+ "share_file_selection_drop_here_label": """
QLabel {
color: #999999;
}""",
-
- 'share_file_selection_drop_count_label': """
+ "share_file_selection_drop_count_label": """
QLabel {
color: #ffffff;
background-color: #f44449;
@@ -345,60 +371,51 @@ class Common(object):
padding: 5px 10px;
border-radius: 10px;
}""",
-
- 'share_file_list_drag_enter': """
+ "share_file_list_drag_enter": """
FileList {
border: 3px solid #538ad0;
}
""",
-
- 'share_file_list_drag_leave': """
+ "share_file_list_drag_leave": """
FileList {
border: none;
}
""",
-
- 'share_file_list_item_size': """
+ "share_file_list_item_size": """
QLabel {
color: #666666;
font-size: 11px;
}""",
-
# Receive mode and child widget styles
- 'receive_file': """
+ "receive_file": """
QWidget {
background-color: #ffffff;
}
""",
-
- 'receive_file_size': """
+ "receive_file_size": """
QLabel {
color: #666666;
font-size: 11px;
}""",
-
# Settings dialog
- 'settings_version': """
+ "settings_version": """
QLabel {
color: #666666;
}""",
-
- 'settings_tor_status': """
+ "settings_tor_status": """
QLabel {
background-color: #ffffff;
color: #000000;
padding: 10px;
}""",
-
- 'settings_whats_this': """
+ "settings_whats_this": """
QLabel {
font-size: 12px;
}""",
-
- 'settings_connect_to_tor': """
+ "settings_connect_to_tor": """
QLabel {
font-style: italic;
- }"""
+ }""",
}
@staticmethod
@@ -408,7 +425,7 @@ class Common(object):
"""
b = os.urandom(num_bytes)
h = hashlib.sha256(b).digest()[:16]
- s = base64.b32encode(h).lower().replace(b'=', b'').decode('utf-8')
+ s = base64.b32encode(h).lower().replace(b"=", b"").decode("utf-8")
if not output_len:
return s
return s[:output_len]
@@ -420,14 +437,14 @@ class Common(object):
"""
thresh = 1024.0
if b < thresh:
- return '{:.1f} B'.format(b)
- units = ('KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB')
+ return "{:.1f} B".format(b)
+ units = ("KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB")
u = 0
b /= thresh
while b >= thresh:
b /= thresh
u += 1
- return '{:.1f} {}'.format(b, units[u])
+ return "{:.1f} {}".format(b, units[u])
@staticmethod
def format_seconds(seconds):
@@ -445,7 +462,7 @@ class Common(object):
human_readable.append("{:.0f}m".format(minutes))
if seconds or not human_readable:
human_readable.append("{:.0f}s".format(seconds))
- return ''.join(human_readable)
+ return "".join(human_readable)
@staticmethod
def estimated_time_remaining(bytes_downloaded, total_bytes, started):
@@ -489,6 +506,7 @@ class AutoStopTimer(threading.Thread):
"""
Background thread sleeps t hours and returns.
"""
+
def __init__(self, common, time):
threading.Thread.__init__(self)
@@ -498,6 +516,8 @@ class AutoStopTimer(threading.Thread):
self.time = time
def run(self):
- self.common.log('AutoStopTimer', 'Server will shut down after {} seconds'.format(self.time))
+ self.common.log(
+ "AutoStopTimer", "Server will shut down after {} seconds".format(self.time)
+ )
time.sleep(self.time)
return 1
diff --git a/onionshare/onion.py b/onionshare/onion.py
index 2f4ddffd..727cf5f1 100644
--- a/onionshare/onion.py
+++ b/onionshare/onion.py
@@ -28,90 +28,113 @@ from distutils.version import LooseVersion as Version
from . import common, strings
from .settings import Settings
+
class TorErrorAutomatic(Exception):
"""
OnionShare is failing to connect and authenticate to the Tor controller,
using automatic settings that should work with Tor Browser.
"""
+
pass
+
class TorErrorInvalidSetting(Exception):
"""
This exception is raised if the settings just don't make sense.
"""
+
pass
+
class TorErrorSocketPort(Exception):
"""
OnionShare can't connect to the Tor controller using the supplied address and port.
"""
+
pass
+
class TorErrorSocketFile(Exception):
"""
OnionShare can't connect to the Tor controller using the supplied socket file.
"""
+
pass
+
class TorErrorMissingPassword(Exception):
"""
OnionShare connected to the Tor controller, but it requires a password.
"""
+
pass
+
class TorErrorUnreadableCookieFile(Exception):
"""
OnionShare connected to the Tor controller, but your user does not have permission
to access the cookie file.
"""
+
pass
+
class TorErrorAuthError(Exception):
"""
OnionShare connected to the address and port, but can't authenticate. It's possible
that a Tor controller isn't listening on this port.
"""
+
pass
+
class TorErrorProtocolError(Exception):
"""
This exception is raised if onionshare connects to the Tor controller, but it
isn't acting like a Tor controller (such as in Whonix).
"""
+
pass
+
class TorTooOld(Exception):
"""
This exception is raised if onionshare needs to use a feature of Tor or stem
(like stealth ephemeral onion services) but the version you have installed
is too old.
"""
+
pass
+
class BundledTorNotSupported(Exception):
"""
This exception is raised if onionshare is set to use the bundled Tor binary,
but it's not supported on that platform, or in dev mode.
"""
+
class BundledTorTimeout(Exception):
"""
This exception is raised if onionshare is set to use the bundled Tor binary,
but Tor doesn't finish connecting promptly.
"""
+
class BundledTorCanceled(Exception):
"""
This exception is raised if onionshare is set to use the bundled Tor binary,
and the user cancels connecting to Tor
"""
+
class BundledTorBroken(Exception):
"""
This exception is raised if onionshare is set to use the bundled Tor binary,
but the process seems to fail to run.
"""
+
class Onion(object):
"""
Onion is an abstraction layer for connecting to the Tor control port and
@@ -126,10 +149,11 @@ class Onion(object):
call this function and pass in a status string while connecting to tor. This
is necessary for status updates to reach the GUI.
"""
+
def __init__(self, common):
self.common = common
- self.common.log('Onion', '__init__')
+ self.common.log("Onion", "__init__")
self.stealth = False
self.service_id = None
@@ -137,13 +161,20 @@ class Onion(object):
self.scheduled_auth_cookie = None
# Is bundled tor supported?
- if (self.common.platform == 'Windows' or self.common.platform == 'Darwin') and getattr(sys, 'onionshare_dev_mode', False):
+ if (
+ self.common.platform == "Windows" or self.common.platform == "Darwin"
+ ) and getattr(sys, "onionshare_dev_mode", False):
self.bundle_tor_supported = False
else:
self.bundle_tor_supported = True
# Set the path of the tor binary, for bundled tor
- (self.tor_path, self.tor_geo_ip_file_path, self.tor_geo_ipv6_file_path, self.obfs4proxy_file_path) = self.common.get_tor_paths()
+ (
+ self.tor_path,
+ self.tor_geo_ip_file_path,
+ self.tor_geo_ipv6_file_path,
+ self.obfs4proxy_file_path,
+ ) = self.common.get_tor_paths()
# The tor process
self.tor_proc = None
@@ -154,8 +185,14 @@ class Onion(object):
# Start out not connected to Tor
self.connected_to_tor = False
- def connect(self, custom_settings=False, config=False, tor_status_update_func=None, connect_timeout=120):
- self.common.log('Onion', 'connect')
+ def connect(
+ self,
+ custom_settings=False,
+ config=False,
+ tor_status_update_func=None,
+ connect_timeout=120,
+ ):
+ self.common.log("Onion", "connect")
# Either use settings that are passed in, or use them from common
if custom_settings:
@@ -171,95 +208,157 @@ class Onion(object):
# The Tor controller
self.c = None
- if self.settings.get('connection_type') == 'bundled':
+ if self.settings.get("connection_type") == "bundled":
if not self.bundle_tor_supported:
- raise BundledTorNotSupported(strings._('settings_error_bundled_tor_not_supported'))
+ raise BundledTorNotSupported(
+ strings._("settings_error_bundled_tor_not_supported")
+ )
# Create a torrc for this session
- self.tor_data_directory = tempfile.TemporaryDirectory(dir=self.common.build_data_dir())
- self.common.log('Onion', 'connect', 'tor_data_directory={}'.format(self.tor_data_directory.name))
+ self.tor_data_directory = tempfile.TemporaryDirectory(
+ dir=self.common.build_data_dir()
+ )
+ self.common.log(
+ "Onion",
+ "connect",
+ "tor_data_directory={}".format(self.tor_data_directory.name),
+ )
# Create the torrc
- with open(self.common.get_resource_path('torrc_template')) as f:
+ with open(self.common.get_resource_path("torrc_template")) as f:
torrc_template = f.read()
- self.tor_cookie_auth_file = os.path.join(self.tor_data_directory.name, 'cookie')
+ self.tor_cookie_auth_file = os.path.join(
+ self.tor_data_directory.name, "cookie"
+ )
try:
self.tor_socks_port = self.common.get_available_port(1000, 65535)
except:
- raise OSError(strings._('no_available_port'))
- self.tor_torrc = os.path.join(self.tor_data_directory.name, 'torrc')
+ raise OSError(strings._("no_available_port"))
+ self.tor_torrc = os.path.join(self.tor_data_directory.name, "torrc")
- if self.common.platform == 'Windows' or self.common.platform == "Darwin":
+ if self.common.platform == "Windows" or self.common.platform == "Darwin":
# Windows doesn't support unix sockets, so it must use a network port.
# macOS can't use unix sockets either because socket filenames are limited to
# 100 chars, and the macOS sandbox forces us to put the socket file in a place
# with a really long path.
- torrc_template += 'ControlPort {{control_port}}\n'
+ torrc_template += "ControlPort {{control_port}}\n"
try:
self.tor_control_port = self.common.get_available_port(1000, 65535)
except:
- raise OSError(strings._('no_available_port'))
+ raise OSError(strings._("no_available_port"))
self.tor_control_socket = None
else:
# Linux and BSD can use unix sockets
- torrc_template += 'ControlSocket {{control_socket}}\n'
+ torrc_template += "ControlSocket {{control_socket}}\n"
self.tor_control_port = None
- self.tor_control_socket = os.path.join(self.tor_data_directory.name, 'control_socket')
-
- torrc_template = torrc_template.replace('{{data_directory}}', self.tor_data_directory.name)
- torrc_template = torrc_template.replace('{{control_port}}', str(self.tor_control_port))
- torrc_template = torrc_template.replace('{{control_socket}}', str(self.tor_control_socket))
- torrc_template = torrc_template.replace('{{cookie_auth_file}}', self.tor_cookie_auth_file)
- torrc_template = torrc_template.replace('{{geo_ip_file}}', self.tor_geo_ip_file_path)
- torrc_template = torrc_template.replace('{{geo_ipv6_file}}', self.tor_geo_ipv6_file_path)
- torrc_template = torrc_template.replace('{{socks_port}}', str(self.tor_socks_port))
-
- with open(self.tor_torrc, 'w') as f:
+ self.tor_control_socket = os.path.join(
+ self.tor_data_directory.name, "control_socket"
+ )
+
+ torrc_template = torrc_template.replace(
+ "{{data_directory}}", self.tor_data_directory.name
+ )
+ torrc_template = torrc_template.replace(
+ "{{control_port}}", str(self.tor_control_port)
+ )
+ torrc_template = torrc_template.replace(
+ "{{control_socket}}", str(self.tor_control_socket)
+ )
+ torrc_template = torrc_template.replace(
+ "{{cookie_auth_file}}", self.tor_cookie_auth_file
+ )
+ torrc_template = torrc_template.replace(
+ "{{geo_ip_file}}", self.tor_geo_ip_file_path
+ )
+ torrc_template = torrc_template.replace(
+ "{{geo_ipv6_file}}", self.tor_geo_ipv6_file_path
+ )
+ torrc_template = torrc_template.replace(
+ "{{socks_port}}", str(self.tor_socks_port)
+ )
+
+ with open(self.tor_torrc, "w") as f:
f.write(torrc_template)
# Bridge support
- if self.settings.get('tor_bridges_use_obfs4'):
- f.write('ClientTransportPlugin obfs4 exec {}\n'.format(self.obfs4proxy_file_path))
- with open(self.common.get_resource_path('torrc_template-obfs4')) as o:
+ if self.settings.get("tor_bridges_use_obfs4"):
+ f.write(
+ "ClientTransportPlugin obfs4 exec {}\n".format(
+ self.obfs4proxy_file_path
+ )
+ )
+ with open(
+ self.common.get_resource_path("torrc_template-obfs4")
+ ) as o:
for line in o:
f.write(line)
- elif self.settings.get('tor_bridges_use_meek_lite_azure'):
- f.write('ClientTransportPlugin meek_lite exec {}\n'.format(self.obfs4proxy_file_path))
- with open(self.common.get_resource_path('torrc_template-meek_lite_azure')) as o:
+ elif self.settings.get("tor_bridges_use_meek_lite_azure"):
+ f.write(
+ "ClientTransportPlugin meek_lite exec {}\n".format(
+ self.obfs4proxy_file_path
+ )
+ )
+ with open(
+ self.common.get_resource_path("torrc_template-meek_lite_azure")
+ ) as o:
for line in o:
f.write(line)
- if self.settings.get('tor_bridges_use_custom_bridges'):
- if 'obfs4' in self.settings.get('tor_bridges_use_custom_bridges'):
- f.write('ClientTransportPlugin obfs4 exec {}\n'.format(self.obfs4proxy_file_path))
- elif 'meek_lite' in self.settings.get('tor_bridges_use_custom_bridges'):
- f.write('ClientTransportPlugin meek_lite exec {}\n'.format(self.obfs4proxy_file_path))
- f.write(self.settings.get('tor_bridges_use_custom_bridges'))
- f.write('\nUseBridges 1')
+ if self.settings.get("tor_bridges_use_custom_bridges"):
+ if "obfs4" in self.settings.get("tor_bridges_use_custom_bridges"):
+ f.write(
+ "ClientTransportPlugin obfs4 exec {}\n".format(
+ self.obfs4proxy_file_path
+ )
+ )
+ elif "meek_lite" in self.settings.get(
+ "tor_bridges_use_custom_bridges"
+ ):
+ f.write(
+ "ClientTransportPlugin meek_lite exec {}\n".format(
+ self.obfs4proxy_file_path
+ )
+ )
+ f.write(self.settings.get("tor_bridges_use_custom_bridges"))
+ f.write("\nUseBridges 1")
# Execute a tor subprocess
start_ts = time.time()
- if self.common.platform == 'Windows':
+ if self.common.platform == "Windows":
# In Windows, hide console window when opening tor.exe subprocess
startupinfo = subprocess.STARTUPINFO()
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
- self.tor_proc = subprocess.Popen([self.tor_path, '-f', self.tor_torrc], stdout=subprocess.PIPE, stderr=subprocess.PIPE, startupinfo=startupinfo)
+ self.tor_proc = subprocess.Popen(
+ [self.tor_path, "-f", self.tor_torrc],
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ startupinfo=startupinfo,
+ )
else:
- self.tor_proc = subprocess.Popen([self.tor_path, '-f', self.tor_torrc], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+ self.tor_proc = subprocess.Popen(
+ [self.tor_path, "-f", self.tor_torrc],
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ )
# Wait for the tor controller to start
time.sleep(2)
# Connect to the controller
try:
- if self.common.platform == 'Windows' or self.common.platform == "Darwin":
+ if (
+ self.common.platform == "Windows"
+ or self.common.platform == "Darwin"
+ ):
self.c = Controller.from_port(port=self.tor_control_port)
self.c.authenticate()
else:
self.c = Controller.from_socket_file(path=self.tor_control_socket)
self.c.authenticate()
except Exception as e:
- raise BundledTorBroken(strings._('settings_error_bundled_tor_broken').format(e.args[0]))
+ raise BundledTorBroken(
+ strings._("settings_error_bundled_tor_broken").format(e.args[0])
+ )
while True:
try:
@@ -268,47 +367,60 @@ class Onion(object):
raise BundledTorCanceled()
res_parts = shlex.split(res)
- progress = res_parts[2].split('=')[1]
- summary = res_parts[4].split('=')[1]
+ progress = res_parts[2].split("=")[1]
+ summary = res_parts[4].split("=")[1]
# "\033[K" clears the rest of the line
- print("Connecting to the Tor network: {}% - {}{}".format(progress, summary, "\033[K"), end="\r")
+ print(
+ "Connecting to the Tor network: {}% - {}{}".format(
+ progress, summary, "\033[K"
+ ),
+ end="\r",
+ )
if callable(tor_status_update_func):
if not tor_status_update_func(progress, summary):
# If the dialog was canceled, stop connecting to Tor
- self.common.log('Onion', 'connect', 'tor_status_update_func returned false, canceling connecting to Tor')
+ self.common.log(
+ "Onion",
+ "connect",
+ "tor_status_update_func returned false, canceling connecting to Tor",
+ )
print()
return False
- if summary == 'Done':
+ if summary == "Done":
print("")
break
time.sleep(0.2)
# If using bridges, it might take a bit longer to connect to Tor
- if self.settings.get('tor_bridges_use_custom_bridges') or \
- self.settings.get('tor_bridges_use_obfs4') or \
- self.settings.get('tor_bridges_use_meek_lite_azure'):
- # Only override timeout if a custom timeout has not been passed in
- if connect_timeout == 120:
- connect_timeout = 150
+ if (
+ self.settings.get("tor_bridges_use_custom_bridges")
+ or self.settings.get("tor_bridges_use_obfs4")
+ or self.settings.get("tor_bridges_use_meek_lite_azure")
+ ):
+ # Only override timeout if a custom timeout has not been passed in
+ if connect_timeout == 120:
+ connect_timeout = 150
if time.time() - start_ts > connect_timeout:
print("")
try:
self.tor_proc.terminate()
- raise BundledTorTimeout(strings._('settings_error_bundled_tor_timeout'))
+ raise BundledTorTimeout(
+ strings._("settings_error_bundled_tor_timeout")
+ )
except FileNotFoundError:
pass
- elif self.settings.get('connection_type') == 'automatic':
+ elif self.settings.get("connection_type") == "automatic":
# Automatically try to guess the right way to connect to Tor Browser
# Try connecting to control port
found_tor = False
# If the TOR_CONTROL_PORT environment variable is set, use that
- env_port = os.environ.get('TOR_CONTROL_PORT')
+ env_port = os.environ.get("TOR_CONTROL_PORT")
if env_port:
try:
self.c = Controller.from_port(port=int(env_port))
@@ -327,11 +439,13 @@ class Onion(object):
pass
# If this still didn't work, try guessing the default socket file path
- socket_file_path = ''
+ socket_file_path = ""
if not found_tor:
try:
- if self.common.platform == 'Darwin':
- socket_file_path = os.path.expanduser('~/Library/Application Support/TorBrowser-Data/Tor/control.socket')
+ if self.common.platform == "Darwin":
+ socket_file_path = os.path.expanduser(
+ "~/Library/Application Support/TorBrowser-Data/Tor/control.socket"
+ )
self.c = Controller.from_socket_file(path=socket_file_path)
found_tor = True
@@ -342,74 +456,108 @@ class Onion(object):
# guessing the socket file name next
if not found_tor:
try:
- if self.common.platform == 'Linux' or self.common.platform == 'BSD':
- socket_file_path = '/run/user/{}/Tor/control.socket'.format(os.geteuid())
- elif self.common.platform == 'Darwin':
- socket_file_path = '/run/user/{}/Tor/control.socket'.format(os.geteuid())
- elif self.common.platform == 'Windows':
+ if self.common.platform == "Linux" or self.common.platform == "BSD":
+ socket_file_path = "/run/user/{}/Tor/control.socket".format(
+ os.geteuid()
+ )
+ elif self.common.platform == "Darwin":
+ socket_file_path = "/run/user/{}/Tor/control.socket".format(
+ os.geteuid()
+ )
+ elif self.common.platform == "Windows":
# Windows doesn't support unix sockets
- raise TorErrorAutomatic(strings._('settings_error_automatic'))
+ raise TorErrorAutomatic(strings._("settings_error_automatic"))
self.c = Controller.from_socket_file(path=socket_file_path)
except:
- raise TorErrorAutomatic(strings._('settings_error_automatic'))
+ raise TorErrorAutomatic(strings._("settings_error_automatic"))
# Try authenticating
try:
self.c.authenticate()
except:
- raise TorErrorAutomatic(strings._('settings_error_automatic'))
+ raise TorErrorAutomatic(strings._("settings_error_automatic"))
else:
# Use specific settings to connect to tor
# Try connecting
try:
- if self.settings.get('connection_type') == 'control_port':
- self.c = Controller.from_port(address=self.settings.get('control_port_address'), port=self.settings.get('control_port_port'))
- elif self.settings.get('connection_type') == 'socket_file':
- self.c = Controller.from_socket_file(path=self.settings.get('socket_file_path'))
+ if self.settings.get("connection_type") == "control_port":
+ self.c = Controller.from_port(
+ address=self.settings.get("control_port_address"),
+ port=self.settings.get("control_port_port"),
+ )
+ elif self.settings.get("connection_type") == "socket_file":
+ self.c = Controller.from_socket_file(
+ path=self.settings.get("socket_file_path")
+ )
else:
raise TorErrorInvalidSetting(strings._("settings_error_unknown"))
except:
- if self.settings.get('connection_type') == 'control_port':
- raise TorErrorSocketPort(strings._("settings_error_socket_port").format(self.settings.get('control_port_address'), self.settings.get('control_port_port')))
+ if self.settings.get("connection_type") == "control_port":
+ raise TorErrorSocketPort(
+ strings._("settings_error_socket_port").format(
+ self.settings.get("control_port_address"),
+ self.settings.get("control_port_port"),
+ )
+ )
else:
- raise TorErrorSocketFile(strings._("settings_error_socket_file").format(self.settings.get('socket_file_path')))
-
+ raise TorErrorSocketFile(
+ strings._("settings_error_socket_file").format(
+ self.settings.get("socket_file_path")
+ )
+ )
# Try authenticating
try:
- if self.settings.get('auth_type') == 'no_auth':
+ if self.settings.get("auth_type") == "no_auth":
self.c.authenticate()
- elif self.settings.get('auth_type') == 'password':
- self.c.authenticate(self.settings.get('auth_password'))
+ elif self.settings.get("auth_type") == "password":
+ self.c.authenticate(self.settings.get("auth_password"))
else:
raise TorErrorInvalidSetting(strings._("settings_error_unknown"))
except MissingPassword:
- raise TorErrorMissingPassword(strings._('settings_error_missing_password'))
+ raise TorErrorMissingPassword(
+ strings._("settings_error_missing_password")
+ )
except UnreadableCookieFile:
- raise TorErrorUnreadableCookieFile(strings._('settings_error_unreadable_cookie_file'))
+ raise TorErrorUnreadableCookieFile(
+ strings._("settings_error_unreadable_cookie_file")
+ )
except AuthenticationFailure:
- raise TorErrorAuthError(strings._('settings_error_auth').format(self.settings.get('control_port_address'), self.settings.get('control_port_port')))
+ raise TorErrorAuthError(
+ strings._("settings_error_auth").format(
+ self.settings.get("control_port_address"),
+ self.settings.get("control_port_port"),
+ )
+ )
# If we made it this far, we should be connected to Tor
self.connected_to_tor = True
# Get the tor version
self.tor_version = self.c.get_version().version_str
- self.common.log('Onion', 'connect', 'Connected to tor {}'.format(self.tor_version))
+ self.common.log(
+ "Onion", "connect", "Connected to tor {}".format(self.tor_version)
+ )
# Do the versions of stem and tor that I'm using support ephemeral onion services?
- list_ephemeral_hidden_services = getattr(self.c, "list_ephemeral_hidden_services", None)
- self.supports_ephemeral = callable(list_ephemeral_hidden_services) and self.tor_version >= '0.2.7.1'
+ list_ephemeral_hidden_services = getattr(
+ self.c, "list_ephemeral_hidden_services", None
+ )
+ self.supports_ephemeral = (
+ callable(list_ephemeral_hidden_services) and self.tor_version >= "0.2.7.1"
+ )
# Do the versions of stem and tor that I'm using support stealth onion services?
try:
- res = self.c.create_ephemeral_hidden_service({1:1}, basic_auth={'onionshare':None}, await_publication=False)
+ res = self.c.create_ephemeral_hidden_service(
+ {1: 1}, basic_auth={"onionshare": None}, await_publication=False
+ )
tmp_service_id = res.service_id
self.c.remove_ephemeral_hidden_service(tmp_service_id)
self.supports_stealth = True
@@ -420,7 +568,7 @@ class Onion(object):
# Does this version of Tor support next-gen ('v3') onions?
# Note, this is the version of Tor where this bug was fixed:
# https://trac.torproject.org/projects/tor/ticket/28619
- self.supports_v3_onions = self.tor_version >= Version('0.3.5.7')
+ self.supports_v3_onions = self.tor_version >= Version("0.3.5.7")
def is_authenticated(self):
"""
@@ -431,37 +579,40 @@ class Onion(object):
else:
return False
-
def start_onion_service(self, port, await_publication, save_scheduled_key=False):
"""
Start a onion service on port 80, pointing to the given port, and
return the onion hostname.
"""
- self.common.log('Onion', 'start_onion_service')
+ self.common.log("Onion", "start_onion_service")
+ # Settings may have changed in the frontend but not updated in our settings object,
+ # such as persistence. Reload the settings now just to be sure.
+ self.settings.load()
+
self.auth_string = None
if not self.supports_ephemeral:
- raise TorTooOld(strings._('error_ephemeral_not_supported'))
+ raise TorTooOld(strings._("error_ephemeral_not_supported"))
if self.stealth and not self.supports_stealth:
- raise TorTooOld(strings._('error_stealth_not_supported'))
+ raise TorTooOld(strings._("error_stealth_not_supported"))
if not save_scheduled_key:
print("Setting up onion service on port {0:d}.".format(int(port)))
if self.stealth:
- if self.settings.get('hidservauth_string'):
- hidservauth_string = self.settings.get('hidservauth_string').split()[2]
- basic_auth = {'onionshare':hidservauth_string}
+ if self.settings.get("hidservauth_string"):
+ hidservauth_string = self.settings.get("hidservauth_string").split()[2]
+ basic_auth = {"onionshare": hidservauth_string}
else:
if self.scheduled_auth_cookie:
- basic_auth = {'onionshare':self.scheduled_auth_cookie}
+ basic_auth = {"onionshare": self.scheduled_auth_cookie}
else:
- basic_auth = {'onionshare':None}
+ basic_auth = {"onionshare": None}
else:
basic_auth = None
- if self.settings.get('private_key'):
- key_content = self.settings.get('private_key')
+ if self.settings.get("private_key"):
+ key_content = self.settings.get("private_key")
if self.is_v2_key(key_content):
key_type = "RSA1024"
else:
@@ -479,7 +630,9 @@ class Onion(object):
else:
key_type = "NEW"
# Work out if we can support v3 onion services, which are preferred
- if self.supports_v3_onions and not self.settings.get('use_legacy_v2_onions'):
+ if self.supports_v3_onions and not self.settings.get(
+ "use_legacy_v2_onions"
+ ):
key_content = "ED25519-V3"
else:
# fall back to v2 onion services
@@ -487,31 +640,48 @@ class Onion(object):
# v3 onions don't yet support basic auth. Our ticket:
# https://github.com/micahflee/onionshare/issues/697
- if key_type == "NEW" and key_content == "ED25519-V3" and not self.settings.get('use_legacy_v2_onions'):
+ if (
+ key_type == "NEW"
+ and key_content == "ED25519-V3"
+ and not self.settings.get("use_legacy_v2_onions")
+ ):
basic_auth = None
self.stealth = False
- debug_message = 'key_type={}'.format(key_type)
+ debug_message = "key_type={}".format(key_type)
if key_type == "NEW":
- debug_message += ', key_content={}'.format(key_content)
- self.common.log('Onion', 'start_onion_service', '{}'.format(debug_message))
+ debug_message += ", key_content={}".format(key_content)
+ self.common.log("Onion", "start_onion_service", "{}".format(debug_message))
try:
if basic_auth != None:
- res = self.c.create_ephemeral_hidden_service({ 80: port }, await_publication=await_publication, basic_auth=basic_auth, key_type=key_type, key_content=key_content)
+ res = self.c.create_ephemeral_hidden_service(
+ {80: port},
+ await_publication=await_publication,
+ basic_auth=basic_auth,
+ key_type=key_type,
+ key_content=key_content,
+ )
else:
# if the stem interface is older than 1.5.0, basic_auth isn't a valid keyword arg
- res = self.c.create_ephemeral_hidden_service({ 80: port }, await_publication=await_publication, key_type=key_type, key_content=key_content)
+ res = self.c.create_ephemeral_hidden_service(
+ {80: port},
+ await_publication=await_publication,
+ key_type=key_type,
+ key_content=key_content,
+ )
except ProtocolError as e:
- raise TorErrorProtocolError(strings._('error_tor_protocol_error').format(e.args[0]))
+ raise TorErrorProtocolError(
+ strings._("error_tor_protocol_error").format(e.args[0])
+ )
self.service_id = res.service_id
- onion_host = self.service_id + '.onion'
+ onion_host = self.service_id + ".onion"
# A new private key was generated and is in the Control port response.
- if self.settings.get('save_private_key'):
- if not self.settings.get('private_key'):
- self.settings.set('private_key', res.private_key)
+ if self.settings.get("save_private_key"):
+ if not self.settings.get("private_key"):
+ self.settings.set("private_key", res.private_key)
# If we were scheduling a future share, register the private key for later re-use
if save_scheduled_key:
@@ -525,24 +695,30 @@ class Onion(object):
# in the first place.
# If we sent the basic_auth (due to a saved hidservauth_string in the settings),
# there is no response here, so use the saved value from settings.
- if self.settings.get('save_private_key'):
- if self.settings.get('hidservauth_string'):
- self.auth_string = self.settings.get('hidservauth_string')
+ if self.settings.get("save_private_key"):
+ if self.settings.get("hidservauth_string"):
+ self.auth_string = self.settings.get("hidservauth_string")
else:
auth_cookie = list(res.client_auth.values())[0]
- self.auth_string = 'HidServAuth {} {}'.format(onion_host, auth_cookie)
- self.settings.set('hidservauth_string', self.auth_string)
+ self.auth_string = "HidServAuth {} {}".format(
+ onion_host, auth_cookie
+ )
+ self.settings.set("hidservauth_string", self.auth_string)
else:
if not self.scheduled_auth_cookie:
auth_cookie = list(res.client_auth.values())[0]
- self.auth_string = 'HidServAuth {} {}'.format(onion_host, auth_cookie)
+ self.auth_string = "HidServAuth {} {}".format(
+ onion_host, auth_cookie
+ )
if save_scheduled_key:
# Register the HidServAuth for the scheduled share
self.scheduled_auth_cookie = auth_cookie
else:
self.scheduled_auth_cookie = None
else:
- self.auth_string = 'HidServAuth {} {}'.format(onion_host, self.scheduled_auth_cookie)
+ self.auth_string = "HidServAuth {} {}".format(
+ onion_host, self.scheduled_auth_cookie
+ )
if not save_scheduled_key:
# We've used the scheduled share's HidServAuth. Reset it to None for future shares
self.scheduled_auth_cookie = None
@@ -551,23 +727,29 @@ class Onion(object):
self.settings.save()
return onion_host
else:
- raise TorErrorProtocolError(strings._('error_tor_protocol_error_unknown'))
+ raise TorErrorProtocolError(strings._("error_tor_protocol_error_unknown"))
def cleanup(self, stop_tor=True):
"""
Stop onion services that were created earlier. If there's a tor subprocess running, kill it.
"""
- self.common.log('Onion', 'cleanup')
+ self.common.log("Onion", "cleanup")
# Cleanup the ephemeral onion services, if we have any
try:
onions = self.c.list_ephemeral_hidden_services()
for onion in onions:
try:
- self.common.log('Onion', 'cleanup', 'trying to remove onion {}'.format(onion))
+ self.common.log(
+ "Onion", "cleanup", "trying to remove onion {}".format(onion)
+ )
self.c.remove_ephemeral_hidden_service(onion)
except:
- self.common.log('Onion', 'cleanup', 'could not remove onion {}.. moving on anyway'.format(onion))
+ self.common.log(
+ "Onion",
+ "cleanup",
+ "could not remove onion {}.. moving on anyway".format(onion),
+ )
pass
except:
pass
@@ -604,14 +786,14 @@ class Onion(object):
"""
Returns a (address, port) tuple for the Tor SOCKS port
"""
- self.common.log('Onion', 'get_tor_socks_port')
+ self.common.log("Onion", "get_tor_socks_port")
- if self.settings.get('connection_type') == 'bundled':
- return ('127.0.0.1', self.tor_socks_port)
- elif self.settings.get('connection_type') == 'automatic':
- return ('127.0.0.1', 9150)
+ if self.settings.get("connection_type") == "bundled":
+ return ("127.0.0.1", self.tor_socks_port)
+ elif self.settings.get("connection_type") == "automatic":
+ return ("127.0.0.1", 9150)
else:
- return (self.settings.get('socks_address'), self.settings.get('socks_port'))
+ return (self.settings.get("socks_address"), self.settings.get("socks_port"))
def is_v2_key(self, key):
"""
diff --git a/onionshare/onionshare.py b/onionshare/onionshare.py
index e746bae1..41a4e5a8 100644
--- a/onionshare/onionshare.py
+++ b/onionshare/onionshare.py
@@ -22,17 +22,19 @@ import os, shutil
from . import common, strings
from .onion import TorTooOld, TorErrorProtocolError
-from .common import AutoStopTimer
+from .common import AutoStopTimer
+
class OnionShare(object):
"""
OnionShare is the main application class. Pass in options and run
start_onion_service and it will do the magic.
"""
+
def __init__(self, common, onion, local_only=False, autostop_timer=0):
self.common = common
- self.common.log('OnionShare', '__init__')
+ self.common.log("OnionShare", "__init__")
# The Onion object
self.onion = onion
@@ -54,7 +56,7 @@ class OnionShare(object):
self.autostop_timer_thread = None
def set_stealth(self, stealth):
- self.common.log('OnionShare', 'set_stealth', 'stealth={}'.format(stealth))
+ self.common.log("OnionShare", "set_stealth", "stealth={}".format(stealth))
self.stealth = stealth
self.onion.stealth = stealth
@@ -66,13 +68,13 @@ class OnionShare(object):
try:
self.port = self.common.get_available_port(17600, 17650)
except:
- raise OSError(strings._('no_available_port'))
+ raise OSError(strings._("no_available_port"))
def start_onion_service(self, await_publication=True, save_scheduled_key=False):
"""
Start the onionshare onion service.
"""
- self.common.log('OnionShare', 'start_onion_service')
+ self.common.log("OnionShare", "start_onion_service")
if not self.port:
self.choose_port()
@@ -81,10 +83,12 @@ class OnionShare(object):
self.autostop_timer_thread = AutoStopTimer(self.common, self.autostop_timer)
if self.local_only:
- self.onion_host = '127.0.0.1:{0:d}'.format(self.port)
+ self.onion_host = "127.0.0.1:{0:d}".format(self.port)
return
- self.onion_host = self.onion.start_onion_service(self.port, await_publication, save_scheduled_key)
+ self.onion_host = self.onion.start_onion_service(
+ self.port, await_publication, save_scheduled_key
+ )
if self.stealth:
self.auth_string = self.onion.auth_string
@@ -93,7 +97,7 @@ class OnionShare(object):
"""
Shut everything down and clean up temporary files, etc.
"""
- self.common.log('OnionShare', 'cleanup')
+ self.common.log("OnionShare", "cleanup")
# Cleanup files
try:
diff --git a/onionshare/settings.py b/onionshare/settings.py
index 16b64a05..25a28350 100644
--- a/onionshare/settings.py
+++ b/onionshare/settings.py
@@ -39,17 +39,22 @@ class Settings(object):
which is to attempt to connect automatically using default Tor Browser
settings.
"""
+
def __init__(self, common, config=False):
self.common = common
- self.common.log('Settings', '__init__')
+ self.common.log("Settings", "__init__")
# If a readable config file was provided, use that instead
if config:
if os.path.isfile(config):
self.filename = config
else:
- self.common.log('Settings', '__init__', 'Supplied config does not exist or is unreadable. Falling back to default location')
+ self.common.log(
+ "Settings",
+ "__init__",
+ "Supplied config does not exist or is unreadable. Falling back to default location",
+ )
self.filename = self.build_filename()
else:
@@ -59,62 +64,67 @@ class Settings(object):
# Dictionary of available languages in this version of OnionShare,
# mapped to the language name, in that language
self.available_locales = {
+ "ar": "العربية", # Arabic
#'bn': 'বাংলা', # Bengali (commented out because not at 90% translation)
- 'ca': 'Català', # Catalan
- 'zh_Hant': '正體中文 (繁體)', # Traditional Chinese
- 'zh_Hans': '中文 (简体)', # Simplified Chinese
- 'da': 'Dansk', # Danish
- 'en': 'English', # English
- 'fi': 'Suomi', # Finnish
- 'fr': 'Français', # French
- 'de': 'Deutsch', # German
- 'el': 'Ελληνικά', # Greek
- 'is': 'Íslenska', # Icelandic
- 'ga': 'Gaeilge', # Irish
- 'it': 'Italiano', # Italian
- 'ja': '日本語', # Japanese
- 'nb': 'Norsk Bokmål', # Norwegian Bokmål
- #'fa': 'فارسی', # Persian (commented out because not at 90% translation)
- 'pl': 'Polski', # Polish
- 'pt_BR': 'Português (Brasil)', # Portuguese Brazil
- 'pt_PT': 'Português (Portugal)', # Portuguese Portugal
- 'ru': 'Русский', # Russian
- 'es': 'Español', # Spanish
- 'sv': 'Svenska', # Swedish
- 'te': 'తెలుగు', # Telugu
- 'tr': 'Türkçe', # Turkish
- 'uk': 'Українська', # Ukrainian
+ "ca": "Català", # Catalan
+ "zh_Hant": "正體中文 (繁體)", # Traditional Chinese
+ "zh_Hans": "中文 (简体)", # Simplified Chinese
+ "da": "Dansk", # Danish
+ "nl": "Nederlands", # Dutch
+ "en": "English", # English
+ # "fi": "Suomi", # Finnish (commented out because not at 90% translation)
+ "fr": "Français", # French
+ "de": "Deutsch", # German
+ "el": "Ελληνικά", # Greek
+ "is": "Íslenska", # Icelandic
+ "ga": "Gaeilge", # Irish
+ "it": "Italiano", # Italian
+ "ja": "日本語", # Japanese
+ "nb": "Norsk Bokmål", # Norwegian Bokmål
+ "fa": "فارسی", # Persian
+ "pl": "Polski", # Polish
+ "pt_BR": "Português (Brasil)", # Portuguese Brazil
+ "pt_PT": "Português (Portugal)", # Portuguese Portugal
+ "ro": "Română", # Romanian
+ "ru": "Русский", # Russian
+ "sr_Latn": "Srpska (latinica)", # Serbian (latin)
+ "es": "Español", # Spanish
+ "sv": "Svenska", # Swedish
+ "te": "తెలుగు", # Telugu
+ "tr": "Türkçe", # Turkish
+ "uk": "Українська", # Ukrainian
}
# These are the default settings. They will get overwritten when loading from disk
self.default_settings = {
- 'version': self.common.version,
- 'connection_type': 'bundled',
- 'control_port_address': '127.0.0.1',
- 'control_port_port': 9051,
- 'socks_address': '127.0.0.1',
- 'socks_port': 9050,
- 'socket_file_path': '/var/run/tor/control',
- 'auth_type': 'no_auth',
- 'auth_password': '',
- 'close_after_first_download': True,
- 'autostop_timer': False,
- 'autostart_timer': False,
- 'use_stealth': False,
- 'use_autoupdate': True,
- 'autoupdate_timestamp': None,
- 'no_bridges': True,
- 'tor_bridges_use_obfs4': False,
- 'tor_bridges_use_meek_lite_azure': False,
- 'tor_bridges_use_custom_bridges': '',
- 'use_legacy_v2_onions': False,
- 'save_private_key': False,
- 'private_key': '',
- 'public_mode': False,
- 'slug': '',
- 'hidservauth_string': '',
- 'data_dir': self.build_default_data_dir(),
- 'locale': None # this gets defined in fill_in_defaults()
+ "version": self.common.version,
+ "connection_type": "bundled",
+ "control_port_address": "127.0.0.1",
+ "control_port_port": 9051,
+ "socks_address": "127.0.0.1",
+ "socks_port": 9050,
+ "socket_file_path": "/var/run/tor/control",
+ "auth_type": "no_auth",
+ "auth_password": "",
+ "close_after_first_download": True,
+ "autostop_timer": False,
+ "autostart_timer": False,
+ "use_stealth": False,
+ "use_autoupdate": True,
+ "autoupdate_timestamp": None,
+ "no_bridges": True,
+ "tor_bridges_use_obfs4": False,
+ "tor_bridges_use_meek_lite_azure": False,
+ "tor_bridges_use_custom_bridges": "",
+ "use_legacy_v2_onions": False,
+ "save_private_key": False,
+ "private_key": "",
+ "public_mode": False,
+ "password": "",
+ "hidservauth_string": "",
+ "data_dir": self.build_default_data_dir(),
+ "csp_header_disabled": False,
+ "locale": None, # this gets defined in fill_in_defaults()
}
self._settings = {}
self.fill_in_defaults()
@@ -129,14 +139,14 @@ class Settings(object):
self._settings[key] = self.default_settings[key]
# Choose the default locale based on the OS preference, and fall-back to English
- if self._settings['locale'] is None:
+ if self._settings["locale"] is None:
language_code, encoding = locale.getdefaultlocale()
# Default to English
if not language_code:
- language_code = 'en_US'
+ language_code = "en_US"
- if language_code == 'pt_PT' and language_code == 'pt_BR':
+ if language_code == "pt_PT" and language_code == "pt_BR":
# Portuguese locales include country code
default_locale = language_code
else:
@@ -144,14 +154,14 @@ class Settings(object):
default_locale = language_code[:2]
if default_locale not in self.available_locales:
- default_locale = 'en'
- self._settings['locale'] = default_locale
+ default_locale = "en"
+ self._settings["locale"] = default_locale
def build_filename(self):
"""
Returns the path of the settings file.
"""
- return os.path.join(self.common.build_data_dir(), 'onionshare.json')
+ return os.path.join(self.common.build_data_dir(), "onionshare.json")
def build_default_data_dir(self):
"""
@@ -162,26 +172,28 @@ class Settings(object):
# We can't use os.path.expanduser() in macOS because in the sandbox it
# returns the path to the sandboxed homedir
real_homedir = pwd.getpwuid(os.getuid()).pw_dir
- return os.path.join(real_homedir, 'OnionShare')
+ return os.path.join(real_homedir, "OnionShare")
elif self.common.platform == "Windows":
# On Windows, os.path.expanduser() needs to use backslash, or else it
# retains the forward slash, which breaks opening the folder in explorer.
- return os.path.expanduser('~\OnionShare')
+ return os.path.expanduser("~\OnionShare")
else:
# All other OSes
- return os.path.expanduser('~/OnionShare')
+ return os.path.expanduser("~/OnionShare")
def load(self):
"""
Load the settings from file.
"""
- self.common.log('Settings', 'load')
+ self.common.log("Settings", "load")
# If the settings file exists, load it
if os.path.exists(self.filename):
try:
- self.common.log('Settings', 'load', 'Trying to load {}'.format(self.filename))
- with open(self.filename, 'r') as f:
+ self.common.log(
+ "Settings", "load", "Trying to load {}".format(self.filename)
+ )
+ with open(self.filename, "r") as f:
self._settings = json.load(f)
self.fill_in_defaults()
except:
@@ -189,7 +201,7 @@ class Settings(object):
# Make sure data_dir exists
try:
- os.makedirs(self.get('data_dir'), exist_ok=True)
+ os.makedirs(self.get("data_dir"), exist_ok=True)
except:
pass
@@ -197,22 +209,24 @@ class Settings(object):
"""
Save settings to file.
"""
- self.common.log('Settings', 'save')
- open(self.filename, 'w').write(json.dumps(self._settings))
- self.common.log('Settings', 'save', 'Settings saved in {}'.format(self.filename))
+ self.common.log("Settings", "save")
+ open(self.filename, "w").write(json.dumps(self._settings, indent=2))
+ self.common.log(
+ "Settings", "save", "Settings saved in {}".format(self.filename)
+ )
def get(self, key):
return self._settings[key]
def set(self, key, val):
# If typecasting int values fails, fallback to default values
- if key == 'control_port_port' or key == 'socks_port':
+ if key == "control_port_port" or key == "socks_port":
try:
val = int(val)
except:
- if key == 'control_port_port':
- val = self.default_settings['control_port_port']
- elif key == 'socks_port':
- val = self.default_settings['socks_port']
+ if key == "control_port_port":
+ val = self.default_settings["control_port_port"]
+ elif key == "socks_port":
+ val = self.default_settings["socks_port"]
self._settings[key] = val
diff --git a/onionshare/strings.py b/onionshare/strings.py
index 643186dd..76360a42 100644
--- a/onionshare/strings.py
+++ b/onionshare/strings.py
@@ -35,14 +35,14 @@ def load_strings(common):
# Load all translations
translations = {}
for locale in common.settings.available_locales:
- locale_dir = common.get_resource_path('locale')
+ locale_dir = common.get_resource_path("locale")
filename = os.path.join(locale_dir, "{}.json".format(locale))
- with open(filename, encoding='utf-8') as f:
+ with open(filename, encoding="utf-8") as f:
translations[locale] = json.load(f)
# Build strings
- default_locale = 'en'
- current_locale = common.settings.get('locale')
+ default_locale = "en"
+ current_locale = common.settings.get("locale")
strings = {}
for s in translations[default_locale]:
if s in translations[current_locale] and translations[current_locale][s] != "":
@@ -57,4 +57,5 @@ def translated(k):
"""
return strings[k]
+
_ = translated
diff --git a/onionshare/web/receive_mode.py b/onionshare/web/receive_mode.py
index bc805445..90f000b9 100644
--- a/onionshare/web/receive_mode.py
+++ b/onionshare/web/receive_mode.py
@@ -8,97 +8,107 @@ from werkzeug.utils import secure_filename
from .. import strings
-class ReceiveModeWeb(object):
+class ReceiveModeWeb:
"""
All of the web logic for receive mode
"""
+
def __init__(self, common, web):
self.common = common
- self.common.log('ReceiveModeWeb', '__init__')
+ self.common.log("ReceiveModeWeb", "__init__")
self.web = web
self.can_upload = True
- self.upload_count = 0
self.uploads_in_progress = []
+ # This tracks the history id
+ self.cur_history_id = 0
+
self.define_routes()
def define_routes(self):
"""
The web app routes for receiving files
"""
- def index_logic():
- self.web.add_request(self.web.REQUEST_LOAD, request.path)
-
- if self.common.settings.get('public_mode'):
- upload_action = '/upload'
- else:
- upload_action = '/{}/upload'.format(self.web.slug)
-
- r = make_response(render_template(
- 'receive.html',
- upload_action=upload_action))
- return self.web.add_security_headers(r)
-
- @self.web.app.route("/<slug_candidate>")
- def index(slug_candidate):
- if not self.can_upload:
- return self.web.error403()
- self.web.check_slug_candidate(slug_candidate)
- return index_logic()
@self.web.app.route("/")
- def index_public():
- if not self.can_upload:
- return self.web.error403()
- if not self.common.settings.get('public_mode'):
- return self.web.error404()
- return index_logic()
+ def index():
+ history_id = self.cur_history_id
+ self.cur_history_id += 1
+ self.web.add_request(
+ self.web.REQUEST_INDIVIDUAL_FILE_STARTED,
+ "{}".format(request.path),
+ {"id": history_id, "status_code": 200},
+ )
+ self.web.add_request(self.web.REQUEST_LOAD, request.path)
+ r = make_response(
+ render_template(
+ "receive.html", static_url_path=self.web.static_url_path
+ )
+ )
+ return self.web.add_security_headers(r)
- def upload_logic(slug_candidate='', ajax=False):
+ @self.web.app.route("/upload", methods=["POST"])
+ def upload(ajax=False):
"""
Handle the upload files POST request, though at this point, the files have
already been uploaded and saved to their correct locations.
"""
- files = request.files.getlist('file[]')
+ files = request.files.getlist("file[]")
filenames = []
for f in files:
- if f.filename != '':
+ if f.filename != "":
filename = secure_filename(f.filename)
filenames.append(filename)
local_path = os.path.join(request.receive_mode_dir, filename)
basename = os.path.basename(local_path)
# Tell the GUI the receive mode directory for this file
- self.web.add_request(self.web.REQUEST_UPLOAD_SET_DIR, request.path, {
- 'id': request.upload_id,
- 'filename': basename,
- 'dir': request.receive_mode_dir
- })
-
- self.common.log('ReceiveModeWeb', 'define_routes', '/upload, uploaded {}, saving to {}'.format(f.filename, local_path))
- print('\n' + "Received: {}".format(local_path))
+ self.web.add_request(
+ self.web.REQUEST_UPLOAD_SET_DIR,
+ request.path,
+ {
+ "id": request.history_id,
+ "filename": basename,
+ "dir": request.receive_mode_dir,
+ },
+ )
+
+ self.common.log(
+ "ReceiveModeWeb",
+ "define_routes",
+ "/upload, uploaded {}, saving to {}".format(
+ f.filename, local_path
+ ),
+ )
+ print("\n" + "Received: {}".format(local_path))
if request.upload_error:
- self.common.log('ReceiveModeWeb', 'define_routes', '/upload, there was an upload error')
-
- self.web.add_request(self.web.REQUEST_ERROR_DATA_DIR_CANNOT_CREATE, request.path, {
- "receive_mode_dir": request.receive_mode_dir
- })
- print("Could not create OnionShare data folder: {}".format(request.receive_mode_dir))
-
- msg = 'Error uploading, please inform the OnionShare user'
+ self.common.log(
+ "ReceiveModeWeb",
+ "define_routes",
+ "/upload, there was an upload error",
+ )
+
+ self.web.add_request(
+ self.web.REQUEST_ERROR_DATA_DIR_CANNOT_CREATE,
+ request.path,
+ {"receive_mode_dir": request.receive_mode_dir},
+ )
+ print(
+ "Could not create OnionShare data folder: {}".format(
+ request.receive_mode_dir
+ )
+ )
+
+ msg = "Error uploading, please inform the OnionShare user"
if ajax:
return json.dumps({"error_flashes": [msg]})
else:
- flash(msg, 'error')
-
- if self.common.settings.get('public_mode'):
- return redirect('/')
- else:
- return redirect('/{}'.format(slug_candidate))
+ flash(msg, "error")
+ return redirect("/")
# Note that flash strings are in English, and not translated, on purpose,
# to avoid leaking the locale of the OnionShare user
@@ -106,67 +116,49 @@ class ReceiveModeWeb(object):
info_flashes = []
if len(filenames) == 0:
- msg = 'No files uploaded'
+ msg = "No files uploaded"
if ajax:
info_flashes.append(msg)
else:
- flash(msg, 'info')
+ flash(msg, "info")
else:
- msg = 'Sent '
+ msg = "Sent "
for filename in filenames:
- msg += '{}, '.format(filename)
- msg = msg.rstrip(', ')
+ msg += "{}, ".format(filename)
+ msg = msg.rstrip(", ")
if ajax:
info_flashes.append(msg)
else:
- flash(msg, 'info')
+ flash(msg, "info")
if self.can_upload:
if ajax:
return json.dumps({"info_flashes": info_flashes})
else:
- if self.common.settings.get('public_mode'):
- path = '/'
- else:
- path = '/{}'.format(slug_candidate)
- return redirect('{}'.format(path))
+ return redirect("/")
else:
if ajax:
- return json.dumps({"new_body": render_template('thankyou.html')})
+ return json.dumps(
+ {
+ "new_body": render_template(
+ "thankyou.html",
+ static_url_path=self.web.static_url_path,
+ )
+ }
+ )
else:
# It was the last upload and the timer ran out
- r = make_response(render_template('thankyou.html'))
+ r = make_response(
+ render_template("thankyou.html"),
+ static_url_path=self.web.static_url_path,
+ )
return self.web.add_security_headers(r)
- @self.web.app.route("/<slug_candidate>/upload", methods=['POST'])
- def upload(slug_candidate):
- if not self.can_upload:
- return self.web.error403()
- self.web.check_slug_candidate(slug_candidate)
- return upload_logic(slug_candidate)
-
- @self.web.app.route("/upload", methods=['POST'])
- def upload_public():
- if not self.can_upload:
- return self.web.error403()
- if not self.common.settings.get('public_mode'):
- return self.web.error404()
- return upload_logic()
-
- @self.web.app.route("/<slug_candidate>/upload-ajax", methods=['POST'])
- def upload_ajax(slug_candidate):
- if not self.can_upload:
- return self.web.error403()
- self.web.check_slug_candidate(slug_candidate)
- return upload_logic(slug_candidate, ajax=True)
-
- @self.web.app.route("/upload-ajax", methods=['POST'])
+ @self.web.app.route("/upload-ajax", methods=["POST"])
def upload_ajax_public():
if not self.can_upload:
return self.web.error403()
- if not self.common.settings.get('public_mode'):
- return self.web.error404()
- return upload_logic(ajax=True)
+ return upload(ajax=True)
class ReceiveModeWSGIMiddleware(object):
@@ -174,13 +166,14 @@ class ReceiveModeWSGIMiddleware(object):
Custom WSGI middleware in order to attach the Web object to environ, so
ReceiveModeRequest can access it.
"""
+
def __init__(self, app, web):
self.app = app
self.web = web
def __call__(self, environ, start_response):
- environ['web'] = self.web
- environ['stop_q'] = self.web.stop_q
+ environ["web"] = self.web
+ environ["stop_q"] = self.web.stop_q
return self.app(environ, start_response)
@@ -190,6 +183,7 @@ class ReceiveModeFile(object):
written to it, in order to track the progress of uploads. It starts out with
a .part file extension, and when it's complete it removes that extension.
"""
+
def __init__(self, request, filename, write_func, close_func):
self.onionshare_request = request
self.onionshare_filename = filename
@@ -197,24 +191,44 @@ class ReceiveModeFile(object):
self.onionshare_close_func = close_func
self.filename = os.path.join(self.onionshare_request.receive_mode_dir, filename)
- self.filename_in_progress = '{}.part'.format(self.filename)
+ self.filename_in_progress = "{}.part".format(self.filename)
# Open the file
self.upload_error = False
try:
- self.f = open(self.filename_in_progress, 'wb+')
+ self.f = open(self.filename_in_progress, "wb+")
except:
# This will only happen if someone is messing with the data dir while
# OnionShare is running, but if it does make sure to throw an error
self.upload_error = True
- self.f = tempfile.TemporaryFile('wb+')
+ self.f = tempfile.TemporaryFile("wb+")
# Make all the file-like methods and attributes actually access the
# TemporaryFile, except for write
- attrs = ['closed', 'detach', 'fileno', 'flush', 'isatty', 'mode',
- 'name', 'peek', 'raw', 'read', 'read1', 'readable', 'readinto',
- 'readinto1', 'readline', 'readlines', 'seek', 'seekable', 'tell',
- 'truncate', 'writable', 'writelines']
+ attrs = [
+ "closed",
+ "detach",
+ "fileno",
+ "flush",
+ "isatty",
+ "mode",
+ "name",
+ "peek",
+ "raw",
+ "read",
+ "read1",
+ "readable",
+ "readinto",
+ "readinto1",
+ "readline",
+ "readlines",
+ "seek",
+ "seekable",
+ "tell",
+ "truncate",
+ "writable",
+ "writelines",
+ ]
for attr in attrs:
setattr(self, attr, getattr(self.f, attr))
@@ -256,25 +270,22 @@ class ReceiveModeRequest(Request):
A custom flask Request object that keeps track of how much data has been
uploaded for each file, for receive mode.
"""
+
def __init__(self, environ, populate_request=True, shallow=False):
super(ReceiveModeRequest, self).__init__(environ, populate_request, shallow)
- self.web = environ['web']
- self.stop_q = environ['stop_q']
+ self.web = environ["web"]
+ self.stop_q = environ["stop_q"]
- self.web.common.log('ReceiveModeRequest', '__init__')
+ self.web.common.log("ReceiveModeRequest", "__init__")
# Prevent running the close() method more than once
self.closed = False
# Is this a valid upload request?
self.upload_request = False
- if self.method == 'POST':
- if self.web.common.settings.get('public_mode'):
- if self.path == '/upload' or self.path == '/upload-ajax':
- self.upload_request = True
- else:
- if self.path == '/{}/upload'.format(self.web.slug) or self.path == '/{}/upload-ajax'.format(self.web.slug):
- self.upload_request = True
+ if self.method == "POST":
+ if self.path == "/upload" or self.path == "/upload-ajax":
+ self.upload_request = True
if self.upload_request:
# No errors yet
@@ -284,7 +295,9 @@ class ReceiveModeRequest(Request):
now = datetime.now()
date_dir = now.strftime("%Y-%m-%d")
time_dir = now.strftime("%H.%M.%S")
- self.receive_mode_dir = os.path.join(self.web.common.settings.get('data_dir'), date_dir, time_dir)
+ self.receive_mode_dir = os.path.join(
+ self.web.common.settings.get("data_dir"), date_dir, time_dir
+ )
# Create that directory, which shouldn't exist yet
try:
@@ -296,7 +309,7 @@ class ReceiveModeRequest(Request):
# Keep going until we find a directory name that's available
i = 1
while True:
- new_receive_mode_dir = '{}-{}'.format(self.receive_mode_dir, i)
+ new_receive_mode_dir = "{}-{}".format(self.receive_mode_dir, i)
try:
os.makedirs(new_receive_mode_dir, 0o700, exist_ok=False)
self.receive_mode_dir = new_receive_mode_dir
@@ -306,15 +319,29 @@ class ReceiveModeRequest(Request):
i += 1
# Failsafe
if i == 100:
- self.web.common.log('ReceiveModeRequest', '__init__', 'Error finding available receive mode directory')
+ self.web.common.log(
+ "ReceiveModeRequest",
+ "__init__",
+ "Error finding available receive mode directory",
+ )
self.upload_error = True
break
except PermissionError:
- self.web.add_request(self.web.REQUEST_ERROR_DATA_DIR_CANNOT_CREATE, request.path, {
- "receive_mode_dir": self.receive_mode_dir
- })
- print("Could not create OnionShare data folder: {}".format(self.receive_mode_dir))
- self.web.common.log('ReceiveModeRequest', '__init__', 'Permission denied creating receive mode directory')
+ self.web.add_request(
+ self.web.REQUEST_ERROR_DATA_DIR_CANNOT_CREATE,
+ request.path,
+ {"receive_mode_dir": self.receive_mode_dir},
+ )
+ print(
+ "Could not create OnionShare data folder: {}".format(
+ self.receive_mode_dir
+ )
+ )
+ self.web.common.log(
+ "ReceiveModeRequest",
+ "__init__",
+ "Permission denied creating receive mode directory",
+ )
self.upload_error = True
# If there's an error so far, finish early
@@ -327,28 +354,33 @@ class ReceiveModeRequest(Request):
# Prevent new uploads if we've said so (timer expired)
if self.web.receive_mode.can_upload:
- # Create an upload_id, attach it to the request
- self.upload_id = self.web.receive_mode.upload_count
-
- self.web.receive_mode.upload_count += 1
+ # Create an history_id, attach it to the request
+ self.history_id = self.web.receive_mode.cur_history_id
+ self.web.receive_mode.cur_history_id += 1
- # Figure out the content length
+ # Figure out the content length
try:
- self.content_length = int(self.headers['Content-Length'])
+ self.content_length = int(self.headers["Content-Length"])
except:
self.content_length = 0
- print("{}: {}".format(
- datetime.now().strftime("%b %d, %I:%M%p"),
- strings._("receive_mode_upload_starting").format(self.web.common.human_readable_filesize(self.content_length))
- ))
+ print(
+ "{}: {}".format(
+ datetime.now().strftime("%b %d, %I:%M%p"),
+ strings._("receive_mode_upload_starting").format(
+ self.web.common.human_readable_filesize(self.content_length)
+ ),
+ )
+ )
# Don't tell the GUI that a request has started until we start receiving files
self.told_gui_about_request = False
self.previous_file = None
- def _get_file_stream(self, total_content_length, content_type, filename=None, content_length=None):
+ def _get_file_stream(
+ self, total_content_length, content_type, filename=None, content_length=None
+ ):
"""
This gets called for each file that gets uploaded, and returns an file-like
writable stream.
@@ -356,24 +388,26 @@ class ReceiveModeRequest(Request):
if self.upload_request:
if not self.told_gui_about_request:
# Tell the GUI about the request
- self.web.add_request(self.web.REQUEST_STARTED, self.path, {
- 'id': self.upload_id,
- 'content_length': self.content_length
- })
- self.web.receive_mode.uploads_in_progress.append(self.upload_id)
+ self.web.add_request(
+ self.web.REQUEST_STARTED,
+ self.path,
+ {"id": self.history_id, "content_length": self.content_length},
+ )
+ self.web.receive_mode.uploads_in_progress.append(self.history_id)
self.told_gui_about_request = True
self.filename = secure_filename(filename)
- self.progress[self.filename] = {
- 'uploaded_bytes': 0,
- 'complete': False
- }
+ self.progress[self.filename] = {"uploaded_bytes": 0, "complete": False}
- f = ReceiveModeFile(self, self.filename, self.file_write_func, self.file_close_func)
+ f = ReceiveModeFile(
+ self, self.filename, self.file_write_func, self.file_close_func
+ )
if f.upload_error:
- self.web.common.log('ReceiveModeRequest', '_get_file_stream', 'Error creating file')
+ self.web.common.log(
+ "ReceiveModeRequest", "_get_file_stream", "Error creating file"
+ )
self.upload_error = True
return f
@@ -388,23 +422,26 @@ class ReceiveModeRequest(Request):
return
self.closed = True
- self.web.common.log('ReceiveModeRequest', 'close')
+ self.web.common.log("ReceiveModeRequest", "close")
try:
if self.told_gui_about_request:
- upload_id = self.upload_id
+ history_id = self.history_id
- if not self.web.stop_q.empty() or not self.progress[self.filename]['complete']:
+ if (
+ not self.web.stop_q.empty()
+ or not self.progress[self.filename]["complete"]
+ ):
# Inform the GUI that the upload has canceled
- self.web.add_request(self.web.REQUEST_UPLOAD_CANCELED, self.path, {
- 'id': upload_id
- })
+ self.web.add_request(
+ self.web.REQUEST_UPLOAD_CANCELED, self.path, {"id": history_id}
+ )
else:
# Inform the GUI that the upload has finished
- self.web.add_request(self.web.REQUEST_UPLOAD_FINISHED, self.path, {
- 'id': upload_id
- })
- self.web.receive_mode.uploads_in_progress.remove(upload_id)
+ self.web.add_request(
+ self.web.REQUEST_UPLOAD_FINISHED, self.path, {"id": history_id}
+ )
+ self.web.receive_mode.uploads_in_progress.remove(history_id)
except AttributeError:
pass
@@ -417,28 +454,34 @@ class ReceiveModeRequest(Request):
return
if self.upload_request:
- self.progress[filename]['uploaded_bytes'] += length
+ self.progress[filename]["uploaded_bytes"] += length
if self.previous_file != filename:
self.previous_file = filename
- print('\r=> {:15s} {}'.format(
- self.web.common.human_readable_filesize(self.progress[filename]['uploaded_bytes']),
- filename
- ), end='')
+ print(
+ "\r=> {:15s} {}".format(
+ self.web.common.human_readable_filesize(
+ self.progress[filename]["uploaded_bytes"]
+ ),
+ filename,
+ ),
+ end="",
+ )
# Update the GUI on the upload progress
if self.told_gui_about_request:
- self.web.add_request(self.web.REQUEST_PROGRESS, self.path, {
- 'id': self.upload_id,
- 'progress': self.progress
- })
+ self.web.add_request(
+ self.web.REQUEST_PROGRESS,
+ self.path,
+ {"id": self.history_id, "progress": self.progress},
+ )
def file_close_func(self, filename, upload_error=False):
"""
This function gets called when a specific file is closed.
"""
- self.progress[filename]['complete'] = True
+ self.progress[filename]["complete"] = True
# If the file tells us there was an upload error, let the request know as well
if upload_error:
diff --git a/onionshare/web/send_base_mode.py b/onionshare/web/send_base_mode.py
new file mode 100644
index 00000000..86d34016
--- /dev/null
+++ b/onionshare/web/send_base_mode.py
@@ -0,0 +1,305 @@
+import os
+import sys
+import tempfile
+import mimetypes
+import gzip
+from flask import Response, request, render_template, make_response
+
+from .. import strings
+
+
+class SendBaseModeWeb:
+ """
+ All of the web logic shared between share and website mode (modes where the user sends files)
+ """
+
+ def __init__(self, common, web):
+ super(SendBaseModeWeb, self).__init__()
+ self.common = common
+ self.web = web
+
+ # Information about the file to be shared
+ self.is_zipped = False
+ self.download_filename = None
+ self.download_filesize = None
+ self.gzip_filename = None
+ self.gzip_filesize = None
+ self.zip_writer = None
+
+ # If "Stop After First Download" is checked (stay_open == False), only allow
+ # one download at a time.
+ self.download_in_progress = False
+
+ # This tracks the history id
+ self.cur_history_id = 0
+
+ self.define_routes()
+ self.init()
+
+ def set_file_info(self, filenames, processed_size_callback=None):
+ """
+ Build a data structure that describes the list of files
+ """
+ # If there's just one folder, replace filenames with a list of files inside that folder
+ if len(filenames) == 1 and os.path.isdir(filenames[0]):
+ filenames = [
+ os.path.join(filenames[0], x) for x in os.listdir(filenames[0])
+ ]
+
+ # Re-initialize
+ self.files = {} # Dictionary mapping file paths to filenames on disk
+ self.root_files = (
+ {}
+ ) # This is only the root files and dirs, as opposed to all of them
+ self.cleanup_filenames = []
+ self.cur_history_id = 0
+ self.file_info = {"files": [], "dirs": []}
+ self.gzip_individual_files = {}
+ self.init()
+
+ # Build the file list
+ for filename in filenames:
+ basename = os.path.basename(filename.rstrip("/"))
+
+ # If it's a filename, add it
+ if os.path.isfile(filename):
+ self.files[basename] = filename
+ self.root_files[basename] = filename
+
+ # If it's a directory, add it recursively
+ elif os.path.isdir(filename):
+ self.root_files[basename + "/"] = filename
+
+ for root, _, nested_filenames in os.walk(filename):
+ # Normalize the root path. So if the directory name is "/home/user/Documents/some_folder",
+ # and it has a nested folder foobar, the root is "/home/user/Documents/some_folder/foobar".
+ # The normalized_root should be "some_folder/foobar"
+ normalized_root = os.path.join(
+ basename, root[len(filename) :].lstrip("/")
+ ).rstrip("/")
+
+ # Add the dir itself
+ self.files[normalized_root + "/"] = root
+
+ # Add the files in this dir
+ for nested_filename in nested_filenames:
+ self.files[
+ os.path.join(normalized_root, nested_filename)
+ ] = os.path.join(root, nested_filename)
+
+ self.set_file_info_custom(filenames, processed_size_callback)
+
+ def directory_listing(self, filenames, path="", filesystem_path=None):
+ # Tell the GUI about the directory listing
+ history_id = self.cur_history_id
+ self.cur_history_id += 1
+ self.web.add_request(
+ self.web.REQUEST_INDIVIDUAL_FILE_STARTED,
+ "/{}".format(path),
+ {"id": history_id, "method": request.method, "status_code": 200},
+ )
+
+ breadcrumbs = [("☗", "/")]
+ parts = path.split("/")[:-1]
+ for i in range(len(parts)):
+ breadcrumbs.append(
+ ("{}".format(parts[i]), "/{}/".format("/".join(parts[0 : i + 1])))
+ )
+ breadcrumbs_leaf = breadcrumbs.pop()[0]
+
+ # If filesystem_path is None, this is the root directory listing
+ files, dirs = self.build_directory_listing(filenames, filesystem_path)
+ r = self.directory_listing_template(
+ path, files, dirs, breadcrumbs, breadcrumbs_leaf
+ )
+ return self.web.add_security_headers(r)
+
+ def build_directory_listing(self, filenames, filesystem_path):
+ files = []
+ dirs = []
+
+ for filename in filenames:
+ if filesystem_path:
+ this_filesystem_path = os.path.join(filesystem_path, filename)
+ else:
+ this_filesystem_path = self.files[filename]
+
+ is_dir = os.path.isdir(this_filesystem_path)
+
+ if is_dir:
+ dirs.append({"basename": filename})
+ else:
+ size = os.path.getsize(this_filesystem_path)
+ size_human = self.common.human_readable_filesize(size)
+ files.append({"basename": filename, "size_human": size_human})
+ return files, dirs
+
+ def stream_individual_file(self, filesystem_path):
+ """
+ Return a flask response that's streaming the download of an individual file, and gzip
+ compressing it if the browser supports it.
+ """
+ use_gzip = self.should_use_gzip()
+
+ # gzip compress the individual file, if it hasn't already been compressed
+ if use_gzip:
+ if filesystem_path not in self.gzip_individual_files:
+ gzip_filename = tempfile.mkstemp("wb+")[1]
+ self._gzip_compress(filesystem_path, gzip_filename, 6, None)
+ self.gzip_individual_files[filesystem_path] = gzip_filename
+
+ # Make sure the gzip file gets cleaned up when onionshare stops
+ self.cleanup_filenames.append(gzip_filename)
+
+ file_to_download = self.gzip_individual_files[filesystem_path]
+ filesize = os.path.getsize(self.gzip_individual_files[filesystem_path])
+ else:
+ file_to_download = filesystem_path
+ filesize = os.path.getsize(filesystem_path)
+
+ path = request.path
+
+ # Tell GUI the individual file started
+ history_id = self.cur_history_id
+ self.cur_history_id += 1
+ self.web.add_request(
+ self.web.REQUEST_INDIVIDUAL_FILE_STARTED,
+ path,
+ {"id": history_id, "filesize": filesize},
+ )
+
+ # Only GET requests are allowed, any other method should fail
+ if request.method != "GET":
+ return self.web.error405()
+
+ def generate():
+ chunk_size = 102400 # 100kb
+
+ fp = open(file_to_download, "rb")
+ done = False
+ while not done:
+ chunk = fp.read(chunk_size)
+ if chunk == b"":
+ done = True
+ else:
+ try:
+ yield chunk
+
+ # Tell GUI the progress
+ downloaded_bytes = fp.tell()
+ percent = (1.0 * downloaded_bytes / filesize) * 100
+ if (
+ not self.web.is_gui
+ or self.common.platform == "Linux"
+ or self.common.platform == "BSD"
+ ):
+ sys.stdout.write(
+ "\r{0:s}, {1:.2f}% ".format(
+ self.common.human_readable_filesize(
+ downloaded_bytes
+ ),
+ percent,
+ )
+ )
+ sys.stdout.flush()
+
+ self.web.add_request(
+ self.web.REQUEST_INDIVIDUAL_FILE_PROGRESS,
+ path,
+ {
+ "id": history_id,
+ "bytes": downloaded_bytes,
+ "filesize": filesize,
+ },
+ )
+ done = False
+ except:
+ # Looks like the download was canceled
+ done = True
+
+ # Tell the GUI the individual file was canceled
+ self.web.add_request(
+ self.web.REQUEST_INDIVIDUAL_FILE_CANCELED,
+ path,
+ {"id": history_id},
+ )
+
+ fp.close()
+
+ if self.common.platform != "Darwin":
+ sys.stdout.write("\n")
+
+ basename = os.path.basename(filesystem_path)
+
+ r = Response(generate())
+ if use_gzip:
+ r.headers.set("Content-Encoding", "gzip")
+ r.headers.set("Content-Length", filesize)
+ r.headers.set("Content-Disposition", "inline", filename=basename)
+ r = self.web.add_security_headers(r)
+ (content_type, _) = mimetypes.guess_type(basename, strict=False)
+ if content_type is not None:
+ r.headers.set("Content-Type", content_type)
+ return r
+
+ def should_use_gzip(self):
+ """
+ Should we use gzip for this browser?
+ """
+ return (not self.is_zipped) and (
+ "gzip" in request.headers.get("Accept-Encoding", "").lower()
+ )
+
+ def _gzip_compress(
+ self, input_filename, output_filename, level, processed_size_callback=None
+ ):
+ """
+ Compress a file with gzip, without loading the whole thing into memory
+ Thanks: https://stackoverflow.com/questions/27035296/python-how-to-gzip-a-large-text-file-without-memoryerror
+ """
+ bytes_processed = 0
+ blocksize = 1 << 16 # 64kB
+ with open(input_filename, "rb") as input_file:
+ output_file = gzip.open(output_filename, "wb", level)
+ while True:
+ if processed_size_callback is not None:
+ processed_size_callback(bytes_processed)
+
+ block = input_file.read(blocksize)
+ if len(block) == 0:
+ break
+ output_file.write(block)
+ bytes_processed += blocksize
+
+ output_file.close()
+
+ def init(self):
+ """
+ Inherited class will implement this
+ """
+ pass
+
+ def define_routes(self):
+ """
+ Inherited class will implement this
+ """
+ pass
+
+ def directory_listing_template(self):
+ """
+ Inherited class will implement this. It should call render_template and return
+ the response.
+ """
+ pass
+
+ def set_file_info_custom(self, filenames, processed_size_callback):
+ """
+ Inherited class will implement this.
+ """
+ pass
+
+ def render_logic(self, path=""):
+ """
+ Inherited class will implement this.
+ """
+ pass
diff --git a/onionshare/web/share_mode.py b/onionshare/web/share_mode.py
index 560a8ba4..21dea639 100644
--- a/onionshare/web/share_mode.py
+++ b/onionshare/web/share_mode.py
@@ -3,65 +3,46 @@ import sys
import tempfile
import zipfile
import mimetypes
-import gzip
from flask import Response, request, render_template, make_response
+from .send_base_mode import SendBaseModeWeb
from .. import strings
-class ShareModeWeb(object):
+class ShareModeWeb(SendBaseModeWeb):
"""
All of the web logic for share mode
"""
- def __init__(self, common, web):
- self.common = common
- self.common.log('ShareModeWeb', '__init__')
-
- self.web = web
-
- # Information about the file to be shared
- self.file_info = []
- self.is_zipped = False
- self.download_filename = None
- self.download_filesize = None
- self.gzip_filename = None
- self.gzip_filesize = None
- self.zip_writer = None
-
- self.download_count = 0
- # If "Stop After First Download" is checked (stay_open == False), only allow
- # one download at a time.
- self.download_in_progress = False
+ def init(self):
+ self.common.log("ShareModeWeb", "init")
- self.define_routes()
+ # Allow downloading individual files if "Stop sharing after files have been sent" is unchecked
+ self.download_individual_files = not self.common.settings.get(
+ "close_after_first_download"
+ )
def define_routes(self):
"""
The web app routes for sharing files
"""
- @self.web.app.route("/<slug_candidate>")
- def index(slug_candidate):
- self.web.check_slug_candidate(slug_candidate)
- return index_logic()
-
- @self.web.app.route("/")
- def index_public():
- if not self.common.settings.get('public_mode'):
- return self.web.error404()
- return index_logic()
-
- def index_logic(slug_candidate=''):
+
+ @self.web.app.route("/", defaults={"path": ""})
+ @self.web.app.route("/<path:path>")
+ def index(path):
"""
Render the template for the onionshare landing page.
"""
self.web.add_request(self.web.REQUEST_LOAD, request.path)
- # Deny new downloads if "Stop After First Download" is checked and there is
+ # Deny new downloads if "Stop sharing after files have been sent" is checked and there is
# currently a download
deny_download = not self.web.stay_open and self.download_in_progress
if deny_download:
- r = make_response(render_template('denied.html'))
+ r = make_response(
+ render_template("denied.html"),
+ static_url_path=self.web.static_url_path,
+ )
return self.web.add_security_headers(r)
# If download is allowed to continue, serve download page
@@ -70,38 +51,10 @@ class ShareModeWeb(object):
else:
self.filesize = self.download_filesize
- if self.web.slug:
- r = make_response(render_template(
- 'send.html',
- slug=self.web.slug,
- file_info=self.file_info,
- filename=os.path.basename(self.download_filename),
- filesize=self.filesize,
- filesize_human=self.common.human_readable_filesize(self.download_filesize),
- is_zipped=self.is_zipped))
- else:
- # If download is allowed to continue, serve download page
- r = make_response(render_template(
- 'send.html',
- file_info=self.file_info,
- filename=os.path.basename(self.download_filename),
- filesize=self.filesize,
- filesize_human=self.common.human_readable_filesize(self.download_filesize),
- is_zipped=self.is_zipped))
- return self.web.add_security_headers(r)
-
- @self.web.app.route("/<slug_candidate>/download")
- def download(slug_candidate):
- self.web.check_slug_candidate(slug_candidate)
- return download_logic()
+ return self.render_logic(path)
@self.web.app.route("/download")
- def download_public():
- if not self.common.settings.get('public_mode'):
- return self.web.error404()
- return download_logic()
-
- def download_logic(slug_candidate=''):
+ def download():
"""
Download the zip file.
"""
@@ -109,16 +62,16 @@ class ShareModeWeb(object):
# currently a download
deny_download = not self.web.stay_open and self.download_in_progress
if deny_download:
- r = make_response(render_template('denied.html'))
+ r = make_response(
+ render_template(
+ "denied.html", static_url_path=self.web.static_url_path
+ )
+ )
return self.web.add_security_headers(r)
- # Each download has a unique id
- download_id = self.download_count
- self.download_count += 1
-
# Prepare some variables to use inside generate() function below
# which is outside of the request context
- shutdown_func = request.environ.get('werkzeug.server.shutdown')
+ shutdown_func = request.environ.get("werkzeug.server.shutdown")
path = request.path
# If this is a zipped file, then serve as-is. If it's not zipped, then,
@@ -133,10 +86,11 @@ class ShareModeWeb(object):
self.filesize = self.download_filesize
# Tell GUI the download started
- self.web.add_request(self.web.REQUEST_STARTED, path, {
- 'id': download_id,
- 'use_gzip': use_gzip
- })
+ history_id = self.cur_history_id
+ self.cur_history_id += 1
+ self.web.add_request(
+ self.web.REQUEST_STARTED, path, {"id": history_id, "use_gzip": use_gzip}
+ )
basename = os.path.basename(self.download_filename)
@@ -147,19 +101,19 @@ class ShareModeWeb(object):
chunk_size = 102400 # 100kb
- fp = open(file_to_download, 'rb')
+ fp = open(file_to_download, "rb")
self.web.done = False
canceled = False
while not self.web.done:
# The user has canceled the download, so stop serving the file
if not self.web.stop_q.empty():
- self.web.add_request(self.web.REQUEST_CANCELED, path, {
- 'id': download_id
- })
+ self.web.add_request(
+ self.web.REQUEST_CANCELED, path, {"id": history_id}
+ )
break
chunk = fp.read(chunk_size)
- if chunk == b'':
+ if chunk == b"":
self.web.done = True
else:
try:
@@ -170,15 +124,26 @@ class ShareModeWeb(object):
percent = (1.0 * downloaded_bytes / self.filesize) * 100
# only output to stdout if running onionshare in CLI mode, or if using Linux (#203, #304)
- if not self.web.is_gui or self.common.platform == 'Linux' or self.common.platform == 'BSD':
+ if (
+ not self.web.is_gui
+ or self.common.platform == "Linux"
+ or self.common.platform == "BSD"
+ ):
sys.stdout.write(
- "\r{0:s}, {1:.2f}% ".format(self.common.human_readable_filesize(downloaded_bytes), percent))
+ "\r{0:s}, {1:.2f}% ".format(
+ self.common.human_readable_filesize(
+ downloaded_bytes
+ ),
+ percent,
+ )
+ )
sys.stdout.flush()
- self.web.add_request(self.web.REQUEST_PROGRESS, path, {
- 'id': download_id,
- 'bytes': downloaded_bytes
- })
+ self.web.add_request(
+ self.web.REQUEST_PROGRESS,
+ path,
+ {"id": history_id, "bytes": downloaded_bytes},
+ )
self.web.done = False
except:
# looks like the download was canceled
@@ -186,13 +151,13 @@ class ShareModeWeb(object):
canceled = True
# tell the GUI the download has canceled
- self.web.add_request(self.web.REQUEST_CANCELED, path, {
- 'id': download_id
- })
+ self.web.add_request(
+ self.web.REQUEST_CANCELED, path, {"id": history_id}
+ )
fp.close()
- if self.common.platform != 'Darwin':
+ if self.common.platform != "Darwin":
sys.stdout.write("\n")
# Download is finished
@@ -205,60 +170,127 @@ class ShareModeWeb(object):
self.web.running = False
try:
if shutdown_func is None:
- raise RuntimeError('Not running with the Werkzeug Server')
+ raise RuntimeError("Not running with the Werkzeug Server")
shutdown_func()
except:
pass
r = Response(generate())
if use_gzip:
- r.headers.set('Content-Encoding', 'gzip')
- r.headers.set('Content-Length', self.filesize)
- r.headers.set('Content-Disposition', 'attachment', filename=basename)
+ r.headers.set("Content-Encoding", "gzip")
+ r.headers.set("Content-Length", self.filesize)
+ r.headers.set("Content-Disposition", "attachment", filename=basename)
r = self.web.add_security_headers(r)
# guess content type
(content_type, _) = mimetypes.guess_type(basename, strict=False)
if content_type is not None:
- r.headers.set('Content-Type', content_type)
+ r.headers.set("Content-Type", content_type)
return r
- def set_file_info(self, filenames, processed_size_callback=None):
- """
- Using the list of filenames being shared, fill in details that the web
- page will need to display. This includes zipping up the file in order to
- get the zip file's name and size.
- """
- self.common.log("ShareModeWeb", "set_file_info")
+ def directory_listing_template(
+ self, path, files, dirs, breadcrumbs, breadcrumbs_leaf
+ ):
+ return make_response(
+ render_template(
+ "send.html",
+ file_info=self.file_info,
+ files=files,
+ dirs=dirs,
+ breadcrumbs=breadcrumbs,
+ breadcrumbs_leaf=breadcrumbs_leaf,
+ filename=os.path.basename(self.download_filename),
+ filesize=self.filesize,
+ filesize_human=self.common.human_readable_filesize(
+ self.download_filesize
+ ),
+ is_zipped=self.is_zipped,
+ static_url_path=self.web.static_url_path,
+ download_individual_files=self.download_individual_files,
+ )
+ )
+
+ def set_file_info_custom(self, filenames, processed_size_callback):
+ self.common.log("ShareModeWeb", "set_file_info_custom")
self.web.cancel_compression = False
+ self.build_zipfile_list(filenames, processed_size_callback)
+
+ def render_logic(self, path=""):
+ if path in self.files:
+ filesystem_path = self.files[path]
+
+ # If it's a directory
+ if os.path.isdir(filesystem_path):
+ # Render directory listing
+ filenames = []
+ for filename in os.listdir(filesystem_path):
+ if os.path.isdir(os.path.join(filesystem_path, filename)):
+ filenames.append(filename + "/")
+ else:
+ filenames.append(filename)
+ filenames.sort()
+ return self.directory_listing(filenames, path, filesystem_path)
+
+ # If it's a file
+ elif os.path.isfile(filesystem_path):
+ if self.download_individual_files:
+ return self.stream_individual_file(filesystem_path)
+ else:
+ history_id = self.cur_history_id
+ self.cur_history_id += 1
+ return self.web.error404(history_id)
+
+ # If it's not a directory or file, throw a 404
+ else:
+ history_id = self.cur_history_id
+ self.cur_history_id += 1
+ return self.web.error404(history_id)
+ else:
+ # Special case loading /
+
+ if path == "":
+ # Root directory listing
+ filenames = list(self.root_files)
+ filenames.sort()
+ return self.directory_listing(filenames, path)
- self.cleanup_filenames = []
+ else:
+ # If the path isn't found, throw a 404
+ history_id = self.cur_history_id
+ self.cur_history_id += 1
+ return self.web.error404(history_id)
- # build file info list
- self.file_info = {'files': [], 'dirs': []}
+ def build_zipfile_list(self, filenames, processed_size_callback=None):
+ self.common.log("ShareModeWeb", "build_zipfile_list")
for filename in filenames:
info = {
- 'filename': filename,
- 'basename': os.path.basename(filename.rstrip('/'))
+ "filename": filename,
+ "basename": os.path.basename(filename.rstrip("/")),
}
if os.path.isfile(filename):
- info['size'] = os.path.getsize(filename)
- info['size_human'] = self.common.human_readable_filesize(info['size'])
- self.file_info['files'].append(info)
+ info["size"] = os.path.getsize(filename)
+ info["size_human"] = self.common.human_readable_filesize(info["size"])
+ self.file_info["files"].append(info)
if os.path.isdir(filename):
- info['size'] = self.common.dir_size(filename)
- info['size_human'] = self.common.human_readable_filesize(info['size'])
- self.file_info['dirs'].append(info)
- self.file_info['files'] = sorted(self.file_info['files'], key=lambda k: k['basename'])
- self.file_info['dirs'] = sorted(self.file_info['dirs'], key=lambda k: k['basename'])
+ info["size"] = self.common.dir_size(filename)
+ info["size_human"] = self.common.human_readable_filesize(info["size"])
+ self.file_info["dirs"].append(info)
+ self.file_info["files"] = sorted(
+ self.file_info["files"], key=lambda k: k["basename"]
+ )
+ self.file_info["dirs"] = sorted(
+ self.file_info["dirs"], key=lambda k: k["basename"]
+ )
# Check if there's only 1 file and no folders
- if len(self.file_info['files']) == 1 and len(self.file_info['dirs']) == 0:
- self.download_filename = self.file_info['files'][0]['filename']
- self.download_filesize = self.file_info['files'][0]['size']
+ if len(self.file_info["files"]) == 1 and len(self.file_info["dirs"]) == 0:
+ self.download_filename = self.file_info["files"][0]["filename"]
+ self.download_filesize = self.file_info["files"][0]["size"]
# Compress the file with gzip now, so we don't have to do it on each request
- self.gzip_filename = tempfile.mkstemp('wb+')[1]
- self._gzip_compress(self.download_filename, self.gzip_filename, 6, processed_size_callback)
+ self.gzip_filename = tempfile.mkstemp("wb+")[1]
+ self._gzip_compress(
+ self.download_filename, self.gzip_filename, 6, processed_size_callback
+ )
self.gzip_filesize = os.path.getsize(self.gzip_filename)
# Make sure the gzip file gets cleaned up when onionshare stops
@@ -268,17 +300,19 @@ class ShareModeWeb(object):
else:
# Zip up the files and folders
- self.zip_writer = ZipWriter(self.common, processed_size_callback=processed_size_callback)
+ self.zip_writer = ZipWriter(
+ self.common, processed_size_callback=processed_size_callback
+ )
self.download_filename = self.zip_writer.zip_filename
- for info in self.file_info['files']:
- self.zip_writer.add_file(info['filename'])
+ for info in self.file_info["files"]:
+ self.zip_writer.add_file(info["filename"])
# Canceling early?
if self.web.cancel_compression:
self.zip_writer.close()
return False
- for info in self.file_info['dirs']:
- if not self.zip_writer.add_dir(info['filename']):
+ for info in self.file_info["dirs"]:
+ if not self.zip_writer.add_dir(info["filename"]):
return False
self.zip_writer.close()
@@ -291,33 +325,6 @@ class ShareModeWeb(object):
return True
- def should_use_gzip(self):
- """
- Should we use gzip for this browser?
- """
- return (not self.is_zipped) and ('gzip' in request.headers.get('Accept-Encoding', '').lower())
-
- def _gzip_compress(self, input_filename, output_filename, level, processed_size_callback=None):
- """
- Compress a file with gzip, without loading the whole thing into memory
- Thanks: https://stackoverflow.com/questions/27035296/python-how-to-gzip-a-large-text-file-without-memoryerror
- """
- bytes_processed = 0
- blocksize = 1 << 16 # 64kB
- with open(input_filename, 'rb') as input_file:
- output_file = gzip.open(output_filename, 'wb', level)
- while True:
- if processed_size_callback is not None:
- processed_size_callback(bytes_processed)
-
- block = input_file.read(blocksize)
- if len(block) == 0:
- break
- output_file.write(block)
- bytes_processed += blocksize
-
- output_file.close()
-
class ZipWriter(object):
"""
@@ -325,6 +332,7 @@ class ZipWriter(object):
with. If a zip_filename is not passed in, it will use the default onionshare
filename.
"""
+
def __init__(self, common, zip_filename=None, processed_size_callback=None):
self.common = common
self.cancel_compression = False
@@ -332,9 +340,11 @@ class ZipWriter(object):
if zip_filename:
self.zip_filename = zip_filename
else:
- self.zip_filename = '{0:s}/onionshare_{1:s}.zip'.format(tempfile.mkdtemp(), self.common.random_string(4, 6))
+ self.zip_filename = "{0:s}/onionshare_{1:s}.zip".format(
+ tempfile.mkdtemp(), self.common.random_string(4, 6)
+ )
- self.z = zipfile.ZipFile(self.zip_filename, 'w', allowZip64=True)
+ self.z = zipfile.ZipFile(self.zip_filename, "w", allowZip64=True)
self.processed_size_callback = processed_size_callback
if self.processed_size_callback is None:
self.processed_size_callback = lambda _: None
@@ -353,7 +363,7 @@ class ZipWriter(object):
"""
Add a directory, and all of its children, to the zip archive.
"""
- dir_to_strip = os.path.dirname(filename.rstrip('/'))+'/'
+ dir_to_strip = os.path.dirname(filename.rstrip("/")) + "/"
for dirpath, dirnames, filenames in os.walk(filename):
for f in filenames:
# Canceling early?
@@ -362,7 +372,7 @@ class ZipWriter(object):
full_filename = os.path.join(dirpath, f)
if not os.path.islink(full_filename):
- arc_filename = full_filename[len(dir_to_strip):]
+ arc_filename = full_filename[len(dir_to_strip) :]
self.z.write(full_filename, arc_filename, zipfile.ZIP_DEFLATED)
self._size += os.path.getsize(full_filename)
self.processed_size_callback(self._size)
diff --git a/onionshare/web/web.py b/onionshare/web/web.py
index edaf75f1..b5b805ec 100644
--- a/onionshare/web/web.py
+++ b/onionshare/web/web.py
@@ -5,54 +5,78 @@ import queue
import socket
import sys
import tempfile
+import requests
from distutils.version import LooseVersion as Version
from urllib.request import urlopen
import flask
-from flask import Flask, request, render_template, abort, make_response, __version__ as flask_version
+from flask import (
+ Flask,
+ request,
+ render_template,
+ abort,
+ make_response,
+ send_file,
+ __version__ as flask_version,
+)
+from flask_httpauth import HTTPBasicAuth
from .. import strings
from .share_mode import ShareModeWeb
from .receive_mode import ReceiveModeWeb, ReceiveModeWSGIMiddleware, ReceiveModeRequest
-
+from .website_mode import WebsiteModeWeb
# Stub out flask's show_server_banner function, to avoiding showing warnings that
# are not applicable to OnionShare
def stubbed_show_server_banner(env, debug, app_import_path, eager_loading):
pass
+
try:
flask.cli.show_server_banner = stubbed_show_server_banner
except:
pass
-class Web(object):
+class Web:
"""
The Web object is the OnionShare web server, powered by flask
"""
+
REQUEST_LOAD = 0
REQUEST_STARTED = 1
REQUEST_PROGRESS = 2
- REQUEST_OTHER = 3
- REQUEST_CANCELED = 4
- REQUEST_RATE_LIMIT = 5
- REQUEST_UPLOAD_FILE_RENAMED = 6
- REQUEST_UPLOAD_SET_DIR = 7
- REQUEST_UPLOAD_FINISHED = 8
- REQUEST_UPLOAD_CANCELED = 9
- REQUEST_ERROR_DATA_DIR_CANNOT_CREATE = 10
-
- def __init__(self, common, is_gui, mode='share'):
+ REQUEST_CANCELED = 3
+ REQUEST_RATE_LIMIT = 4
+ REQUEST_UPLOAD_FILE_RENAMED = 5
+ REQUEST_UPLOAD_SET_DIR = 6
+ REQUEST_UPLOAD_FINISHED = 7
+ REQUEST_UPLOAD_CANCELED = 8
+ REQUEST_INDIVIDUAL_FILE_STARTED = 9
+ REQUEST_INDIVIDUAL_FILE_PROGRESS = 10
+ REQUEST_INDIVIDUAL_FILE_CANCELED = 11
+ REQUEST_ERROR_DATA_DIR_CANNOT_CREATE = 12
+ REQUEST_OTHER = 13
+ REQUEST_INVALID_PASSWORD = 14
+
+ def __init__(self, common, is_gui, mode="share"):
self.common = common
- self.common.log('Web', '__init__', 'is_gui={}, mode={}'.format(is_gui, mode))
+ self.common.log("Web", "__init__", "is_gui={}, mode={}".format(is_gui, mode))
# The flask app
- self.app = Flask(__name__,
- static_folder=self.common.get_resource_path('static'),
- template_folder=self.common.get_resource_path('templates'))
+ self.app = Flask(
+ __name__,
+ static_folder=self.common.get_resource_path("static"),
+ static_url_path="/static_".format(
+ self.common.random_string(16)
+ ), # randomize static_url_path to avoid making /static unusable
+ template_folder=self.common.get_resource_path("templates"),
+ )
self.app.secret_key = self.common.random_string(8)
+ self.generate_static_url_path()
+ self.auth = HTTPBasicAuth()
+ self.auth.error_handler(self.error401)
# Verbose mode?
if self.common.verbose:
@@ -68,7 +92,7 @@ class Web(object):
# Are we using receive mode?
self.mode = mode
- if self.mode == 'receive':
+ if self.mode == "receive":
# Use custom WSGI middleware, to modify environ
self.app.wsgi_app = ReceiveModeWSGIMiddleware(self.app.wsgi_app, self)
# Use a custom Request class to track upload progess
@@ -78,27 +102,27 @@ class Web(object):
# by default. To prevent content injection through template variables in
# earlier versions of Flask, we force autoescaping in the Jinja2 template
# engine if we detect a Flask version with insecure default behavior.
- if Version(flask_version) < Version('0.11'):
+ if Version(flask_version) < Version("0.11"):
# Monkey-patch in the fix from https://github.com/pallets/flask/commit/99c99c4c16b1327288fd76c44bc8635a1de452bc
Flask.select_jinja_autoescape = self._safe_select_jinja_autoescape
self.security_headers = [
- ('Content-Security-Policy', 'default-src \'self\'; style-src \'self\'; script-src \'self\'; img-src \'self\' data:;'),
- ('X-Frame-Options', 'DENY'),
- ('X-Xss-Protection', '1; mode=block'),
- ('X-Content-Type-Options', 'nosniff'),
- ('Referrer-Policy', 'no-referrer'),
- ('Server', 'OnionShare')
+ ("X-Frame-Options", "DENY"),
+ ("X-Xss-Protection", "1; mode=block"),
+ ("X-Content-Type-Options", "nosniff"),
+ ("Referrer-Policy", "no-referrer"),
+ ("Server", "OnionShare"),
]
self.q = queue.Queue()
- self.slug = None
- self.error404_count = 0
+ self.password = None
+
+ self.reset_invalid_passwords()
self.done = False
# shutting down the server only works within the context of flask, so the easiest way to do it is over http
- self.shutdown_slug = self.common.random_string(16)
+ self.shutdown_password = self.common.random_string(16)
# Keep track if the server is running
self.running = False
@@ -109,59 +133,143 @@ class Web(object):
# Create the mode web object, which defines its own routes
self.share_mode = None
self.receive_mode = None
- if self.mode == 'receive':
- self.receive_mode = ReceiveModeWeb(self.common, self)
- elif self.mode == 'share':
+ self.website_mode = None
+ if self.mode == "share":
self.share_mode = ShareModeWeb(self.common, self)
-
+ elif self.mode == "receive":
+ self.receive_mode = ReceiveModeWeb(self.common, self)
+ elif self.mode == "website":
+ self.website_mode = WebsiteModeWeb(self.common, self)
+
+ def get_mode(self):
+ if self.mode == "share":
+ return self.share_mode
+ elif self.mode == "receive":
+ return self.receive_mode
+ elif self.mode == "website":
+ return self.website_mode
+ else:
+ return None
+
+ def generate_static_url_path(self):
+ # The static URL path has a 128-bit random number in it to avoid having name
+ # collisions with files that might be getting shared
+ self.static_url_path = "/static_{}".format(self.common.random_string(16))
+ self.common.log(
+ "Web",
+ "generate_static_url_path",
+ "new static_url_path is {}".format(self.static_url_path),
+ )
+
+ # Update the flask route to handle the new static URL path
+ self.app.static_url_path = self.static_url_path
+ self.app.add_url_rule(
+ self.static_url_path + "/<path:filename>",
+ endpoint="static",
+ view_func=self.app.send_static_file,
+ )
def define_common_routes(self):
"""
- Common web app routes between sending and receiving
+ Common web app routes between all modes.
"""
- @self.app.errorhandler(404)
- def page_not_found(e):
- """
- 404 error page.
- """
- return self.error404()
- @self.app.route("/<slug_candidate>/shutdown")
- def shutdown(slug_candidate):
- """
- Stop the flask web server, from the context of an http request.
- """
- self.check_shutdown_slug_candidate(slug_candidate)
- self.force_shutdown()
- return ""
+ @self.auth.get_password
+ def get_pw(username):
+ if username == "onionshare":
+ return self.password
+ else:
+ return None
+
+ @self.app.before_request
+ def conditional_auth_check():
+ # Allow static files without basic authentication
+ if request.path.startswith(self.static_url_path + "/"):
+ return None
+
+ # If public mode is disabled, require authentication
+ if not self.common.settings.get("public_mode"):
+
+ @self.auth.login_required
+ def _check_login():
+ return None
+
+ return _check_login()
- @self.app.route("/noscript-xss-instructions")
- def noscript_xss_instructions():
+ @self.app.errorhandler(404)
+ def not_found(e):
+ mode = self.get_mode()
+ history_id = mode.cur_history_id
+ mode.cur_history_id += 1
+ return self.error404(history_id)
+
+ @self.app.route("/<password_candidate>/shutdown")
+ def shutdown(password_candidate):
"""
- Display instructions for disabling Tor Browser's NoScript XSS setting
+ Stop the flask web server, from the context of an http request.
"""
- r = make_response(render_template('receive_noscript_xss.html'))
- return self.add_security_headers(r)
-
- def error404(self):
- self.add_request(Web.REQUEST_OTHER, request.path)
- if request.path != '/favicon.ico':
- self.error404_count += 1
+ if password_candidate == self.shutdown_password:
+ self.force_shutdown()
+ return ""
+ abort(404)
- # In receive mode, with public mode enabled, skip rate limiting 404s
- if not self.common.settings.get('public_mode'):
- if self.error404_count == 20:
- self.add_request(Web.REQUEST_RATE_LIMIT, request.path)
+ if self.mode != "website":
+
+ @self.app.route("/favicon.ico")
+ def favicon():
+ return send_file(
+ "{}/img/favicon.ico".format(self.common.get_resource_path("static"))
+ )
+
+ def error401(self):
+ auth = request.authorization
+ if auth:
+ if (
+ auth["username"] == "onionshare"
+ and auth["password"] not in self.invalid_passwords
+ ):
+ print("Invalid password guess: {}".format(auth["password"]))
+ self.add_request(Web.REQUEST_INVALID_PASSWORD, data=auth["password"])
+
+ self.invalid_passwords.append(auth["password"])
+ self.invalid_passwords_count += 1
+
+ if self.invalid_passwords_count == 20:
+ self.add_request(Web.REQUEST_RATE_LIMIT)
self.force_shutdown()
- print("Someone has made too many wrong attempts on your address, which means they could be trying to guess it, so OnionShare has stopped the server. Start sharing again and send the recipient a new address to share.")
+ print(
+ "Someone has made too many wrong attempts to guess your password, so OnionShare has stopped the server. Start sharing again and send the recipient a new address to share."
+ )
- r = make_response(render_template('404.html'), 404)
+ r = make_response(
+ render_template("401.html", static_url_path=self.static_url_path), 401
+ )
return self.add_security_headers(r)
def error403(self):
self.add_request(Web.REQUEST_OTHER, request.path)
+ r = make_response(
+ render_template("403.html", static_url_path=self.static_url_path), 403
+ )
+ return self.add_security_headers(r)
+
+ def error404(self, history_id):
+ self.add_request(
+ self.REQUEST_INDIVIDUAL_FILE_STARTED,
+ "{}".format(request.path),
+ {"id": history_id, "status_code": 404},
+ )
- r = make_response(render_template('403.html'), 403)
+ self.add_request(Web.REQUEST_OTHER, request.path)
+ r = make_response(
+ render_template("404.html", static_url_path=self.static_url_path), 404
+ )
+ return self.add_security_headers(r)
+
+ def error405(self):
+ r = make_response(
+ render_template("405.html", static_url_path=self.static_url_path), 405
+ )
return self.add_security_headers(r)
def add_security_headers(self, r):
@@ -170,52 +278,61 @@ class Web(object):
"""
for header, value in self.security_headers:
r.headers.set(header, value)
+ # Set a CSP header unless in website mode and the user has disabled it
+ if (
+ not self.common.settings.get("csp_header_disabled")
+ or self.mode != "website"
+ ):
+ r.headers.set(
+ "Content-Security-Policy",
+ "default-src 'self'; style-src 'self'; script-src 'self'; img-src 'self' data:;",
+ )
return r
def _safe_select_jinja_autoescape(self, filename):
if filename is None:
return True
- return filename.endswith(('.html', '.htm', '.xml', '.xhtml'))
+ return filename.endswith((".html", ".htm", ".xml", ".xhtml"))
- def add_request(self, request_type, path, data=None):
+ def add_request(self, request_type, path=None, data=None):
"""
Add a request to the queue, to communicate with the GUI.
"""
- self.q.put({
- 'type': request_type,
- 'path': path,
- 'data': data
- })
-
- def generate_slug(self, persistent_slug=None):
- self.common.log('Web', 'generate_slug', 'persistent_slug={}'.format(persistent_slug))
- if persistent_slug != None and persistent_slug != '':
- self.slug = persistent_slug
- self.common.log('Web', 'generate_slug', 'persistent_slug sent, so slug is: "{}"'.format(self.slug))
+ self.q.put({"type": request_type, "path": path, "data": data})
+
+ def generate_password(self, persistent_password=None):
+ self.common.log(
+ "Web",
+ "generate_password",
+ "persistent_password={}".format(persistent_password),
+ )
+ if persistent_password != None and persistent_password != "":
+ self.password = persistent_password
+ self.common.log(
+ "Web",
+ "generate_password",
+ 'persistent_password sent, so password is: "{}"'.format(self.password),
+ )
else:
- self.slug = self.common.build_slug()
- self.common.log('Web', 'generate_slug', 'built random slug: "{}"'.format(self.slug))
+ self.password = self.common.build_password()
+ self.common.log(
+ "Web",
+ "generate_password",
+ 'built random password: "{}"'.format(self.password),
+ )
def verbose_mode(self):
"""
Turn on verbose mode, which will log flask errors to a file.
"""
- flask_log_filename = os.path.join(self.common.build_data_dir(), 'flask.log')
+ flask_log_filename = os.path.join(self.common.build_data_dir(), "flask.log")
log_handler = logging.FileHandler(flask_log_filename)
log_handler.setLevel(logging.WARNING)
self.app.logger.addHandler(log_handler)
- def check_slug_candidate(self, slug_candidate):
- self.common.log('Web', 'check_slug_candidate: slug_candidate={}'.format(slug_candidate))
- if self.common.settings.get('public_mode'):
- abort(404)
- if not hmac.compare_digest(self.slug, slug_candidate):
- abort(404)
-
- def check_shutdown_slug_candidate(self, slug_candidate):
- self.common.log('Web', 'check_shutdown_slug_candidate: slug_candidate={}'.format(slug_candidate))
- if not hmac.compare_digest(self.shutdown_slug, slug_candidate):
- abort(404)
+ def reset_invalid_passwords(self):
+ self.invalid_passwords_count = 0
+ self.invalid_passwords = []
def force_shutdown(self):
"""
@@ -223,19 +340,25 @@ class Web(object):
"""
# Shutdown the flask service
try:
- func = request.environ.get('werkzeug.server.shutdown')
+ func = request.environ.get("werkzeug.server.shutdown")
if func is None:
- raise RuntimeError('Not running with the Werkzeug Server')
+ raise RuntimeError("Not running with the Werkzeug Server")
func()
except:
pass
self.running = False
- def start(self, port, stay_open=False, public_mode=False, slug=None):
+ def start(self, port, stay_open=False, public_mode=False, password=None):
"""
Start the flask web server.
"""
- self.common.log('Web', 'start', 'port={}, stay_open={}, public_mode={}, slug={}'.format(port, stay_open, public_mode, slug))
+ self.common.log(
+ "Web",
+ "start",
+ "port={}, stay_open={}, public_mode={}, password={}".format(
+ port, stay_open, public_mode, password
+ ),
+ )
self.stay_open = stay_open
@@ -247,10 +370,10 @@ class Web(object):
pass
# In Whonix, listen on 0.0.0.0 instead of 127.0.0.1 (#220)
- if os.path.exists('/usr/share/anon-ws-base-files/workstation'):
- host = '0.0.0.0'
+ if os.path.exists("/usr/share/anon-ws-base-files/workstation"):
+ host = "0.0.0.0"
else:
- host = '127.0.0.1'
+ host = "127.0.0.1"
self.running = True
self.app.run(host=host, port=port, threaded=True)
@@ -259,22 +382,18 @@ class Web(object):
"""
Stop the flask web server by loading /shutdown.
"""
- self.common.log('Web', 'stop', 'stopping server')
+ self.common.log("Web", "stop", "stopping server")
# Let the mode know that the user stopped the server
self.stop_q.put(True)
- # Reset any slug that was in use
- self.slug = None
-
- # To stop flask, load http://127.0.0.1:<port>/<shutdown_slug>/shutdown
+ # To stop flask, load http://shutdown:[shutdown_password]@127.0.0.1/[shutdown_password]/shutdown
+ # (We're putting the shutdown_password in the path as well to make routing simpler)
if self.running:
- try:
- s = socket.socket()
- s.connect(('127.0.0.1', port))
- s.sendall('GET /{0:s}/shutdown HTTP/1.1\r\n\r\n'.format(self.shutdown_slug))
- except:
- try:
- urlopen('http://127.0.0.1:{0:d}/{1:s}/shutdown'.format(port, self.shutdown_slug)).read()
- except:
- pass
+ requests.get(
+ "http://127.0.0.1:{}/{}/shutdown".format(port, self.shutdown_password),
+ auth=requests.auth.HTTPBasicAuth("onionshare", self.password),
+ )
+
+ # Reset any password that was in use
+ self.password = None
diff --git a/onionshare/web/website_mode.py b/onionshare/web/website_mode.py
new file mode 100644
index 00000000..61b6d2c6
--- /dev/null
+++ b/onionshare/web/website_mode.py
@@ -0,0 +1,104 @@
+import os
+import sys
+import tempfile
+import mimetypes
+from flask import Response, request, render_template, make_response
+
+from .send_base_mode import SendBaseModeWeb
+from .. import strings
+
+
+class WebsiteModeWeb(SendBaseModeWeb):
+ """
+ All of the web logic for website mode
+ """
+
+ def init(self):
+ pass
+
+ def define_routes(self):
+ """
+ The web app routes for sharing a website
+ """
+
+ @self.web.app.route("/", defaults={"path": ""})
+ @self.web.app.route("/<path:path>")
+ def path_public(path):
+ return path_logic(path)
+
+ def path_logic(path=""):
+ """
+ Render the onionshare website.
+ """
+ return self.render_logic(path)
+
+ def directory_listing_template(
+ self, path, files, dirs, breadcrumbs, breadcrumbs_leaf
+ ):
+ return make_response(
+ render_template(
+ "listing.html",
+ path=path,
+ files=files,
+ dirs=dirs,
+ breadcrumbs=breadcrumbs,
+ breadcrumbs_leaf=breadcrumbs_leaf,
+ static_url_path=self.web.static_url_path,
+ )
+ )
+
+ def set_file_info_custom(self, filenames, processed_size_callback):
+ self.common.log("WebsiteModeWeb", "set_file_info_custom")
+ self.web.cancel_compression = True
+
+ def render_logic(self, path=""):
+ if path in self.files:
+ filesystem_path = self.files[path]
+
+ # If it's a directory
+ if os.path.isdir(filesystem_path):
+ # Is there an index.html?
+ index_path = os.path.join(path, "index.html")
+ if index_path in self.files:
+ # Render it
+ return self.stream_individual_file(self.files[index_path])
+
+ else:
+ # Otherwise, render directory listing
+ filenames = []
+ for filename in os.listdir(filesystem_path):
+ if os.path.isdir(os.path.join(filesystem_path, filename)):
+ filenames.append(filename + "/")
+ else:
+ filenames.append(filename)
+ filenames.sort()
+ return self.directory_listing(filenames, path, filesystem_path)
+
+ # If it's a file
+ elif os.path.isfile(filesystem_path):
+ return self.stream_individual_file(filesystem_path)
+
+ # If it's not a directory or file, throw a 404
+ else:
+ history_id = self.cur_history_id
+ self.cur_history_id += 1
+ return self.web.error404(history_id)
+ else:
+ # Special case loading /
+
+ if path == "":
+ index_path = "index.html"
+ if index_path in self.files:
+ # Render it
+ return self.stream_individual_file(self.files[index_path])
+ else:
+ # Root directory listing
+ filenames = list(self.root_files)
+ filenames.sort()
+ return self.directory_listing(filenames, path)
+
+ else:
+ # If the path isn't found, throw a 404
+ history_id = self.cur_history_id
+ self.cur_history_id += 1
+ return self.web.error404(history_id)