diff --git a/.gitignore b/.gitignore index 20342555..712484d6 100644 --- a/.gitignore +++ b/.gitignore @@ -27,6 +27,7 @@ pip-log.txt .coverage .tox nosetests.xml +.cache # Translations *.mo diff --git a/MANIFEST.in b/MANIFEST.in index 64eca8f9..f4d1c078 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,11 +1,12 @@ include LICENSE include README.md include BUILD.md -include resources/* -include resources/images/* -include resources/locale/* -include resources/html/* +include share/* +include share/images/* +include share/locale/* +include share/html/* include install/onionshare.desktop include install/onionshare.appdata.xml include install/onionshare80.xpm include install/scripts/onionshare-nautilus.py +include test/*.py diff --git a/install/scripts/onionshare-nautilus.py b/install/scripts/onionshare-nautilus.py index db701c40..d5e83919 100644 --- a/install/scripts/onionshare-nautilus.py +++ b/install/scripts/onionshare-nautilus.py @@ -1,6 +1,7 @@ -#!/usr/bin/env python - import os +import sys +import json +import locale import subprocess import urllib import gi @@ -12,7 +13,55 @@ from gi.repository import GObject # Put me in /usr/share/nautilus-python/extensions/ class OnionShareExtension(GObject.GObject, Nautilus.MenuProvider): def __init__(self): - pass + # Get the localized string for "Share via OnionShare" label + self.label = None + default_label = 'Share via OnionShare' + + try: + # Re-implement localization in python2 + default_locale = 'en' + locale_dir = os.path.join(sys.prefix, 'share/onionshare/locale') + if os.path.exists(locale_dir): + # Load all translations + strings = {} + translations = {} + for filename in os.listdir(locale_dir): + abs_filename = os.path.join(locale_dir, filename) + lang, ext = os.path.splitext(filename) + if ext == '.json': + with open(abs_filename) as f: + translations[lang] = json.load(f) + + strings = translations[default_locale] + lc, enc = locale.getdefaultlocale() + if lc: + lang = lc[:2] + if lang in translations: + # if a string doesn't exist, fallback to English + for key in translations[default_locale]: + if key in translations[lang]: + strings[key] = translations[lang][key] + + self.label = strings['share_via_onionshare'] + + except: + self.label = default_label + + if not self.label: + self.label = default_label + + """ + # This more elegant solution will only work if nautilus is using python3, and onionshare is installed system-wide. + # But nautilus is using python2, so this is commented out. + try: + import onionshare + onionshare.strings.load_strings(onionshare.common) + self.label = onionshare.strings._('share_via_onionshare') + except: + import sys + print('python version: {}').format(sys.version) + self.label = 'Share via OnionShare' + """ def url2path(self,url): file_uri = url.get_activation_uri() @@ -31,7 +80,7 @@ class OnionShareExtension(GObject.GObject, Nautilus.MenuProvider): def get_file_items(self, window, files): menuitem = Nautilus.MenuItem(name='OnionShare::Nautilus', - label='Share via OnionShare', + label=self.label, tip='', icon='') menu = Nautilus.Menu() diff --git a/onionshare/common.py b/onionshare/common.py index 8f4d257e..3f7bf722 100644 --- a/onionshare/common.py +++ b/onionshare/common.py @@ -20,7 +20,6 @@ along with this program. If not, see . import base64 import hashlib import inspect -import math import os import platform import random @@ -70,9 +69,12 @@ def get_resource_path(filename): 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') + 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 p == 'Linux' and sys.argv and sys.argv[0].startswith(sys.prefix): - # OnionShare is installed systemwide in Linux + elif p == 'Linux': + # Assume OnionShare is installed systemwide in Linux, since we're not running in dev mode prefix = os.path.join(sys.prefix, 'share/onionshare') elif getattr(sys, 'frozen', False): @@ -144,14 +146,14 @@ def human_readable_filesize(b): """ thresh = 1024.0 if b < thresh: - return '{0:.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 '{0:.1f} {1:s}'.format(round(b, 1), units[u]) + return '{:.1f} {}'.format(b, units[u]) def format_seconds(seconds): diff --git a/onionshare/onion.py b/onionshare/onion.py index efcd7ae2..aedd98c2 100644 --- a/onionshare/onion.py +++ b/onionshare/onion.py @@ -433,6 +433,13 @@ class Onion(object): self.stealth = False self.service_id = None + try: + # Delete the temporary tor data directory + self.tor_data_directory.cleanup() + except AttributeError: + # Skip if cleanup was somehow run before connect + pass + def get_tor_socks_port(self): """ Returns a (address, port) tuple for the Tor SOCKS port diff --git a/onionshare/settings.py b/onionshare/settings.py index 18e7dd26..408c8bdc 100644 --- a/onionshare/settings.py +++ b/onionshare/settings.py @@ -18,10 +18,13 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ -import platform, os, json +import json +import os +import platform from . import strings, common + class Settings(object): """ This class stores all of the settings for OnionShare, specifically for how @@ -95,7 +98,7 @@ class Settings(object): try: common.log('Settings', 'load', 'Trying to load {}'.format(self.filename)) with open(self.filename, 'r') as f: - self._settings = json.loads(f.read()) + self._settings = json.load(f) self.fill_in_defaults() except: pass diff --git a/onionshare/strings.py b/onionshare/strings.py index 9b26b4b1..7a1f08a5 100644 --- a/onionshare/strings.py +++ b/onionshare/strings.py @@ -17,17 +17,19 @@ GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . """ -import json, locale, os +import json +import locale +import os strings = {} + def load_strings(common, default="en"): """ Loads translated strings and fallback to English if the translation does not exist. """ global strings - p = common.get_platform() # find locale dir locale_dir = common.get_resource_path('locale') @@ -37,10 +39,9 @@ def load_strings(common, default="en"): for filename in os.listdir(locale_dir): abs_filename = os.path.join(locale_dir, filename) lang, ext = os.path.splitext(filename) - if abs_filename.endswith('.json'): + if ext == '.json': with open(abs_filename, encoding='utf-8') as f: - lang_json = f.read() - translations[lang] = json.loads(lang_json) + translations[lang] = json.load(f) strings = translations[default] lc, enc = locale.getdefaultlocale() diff --git a/onionshare/web.py b/onionshare/web.py index aec86bf4..4ae10221 100644 --- a/onionshare/web.py +++ b/onionshare/web.py @@ -17,12 +17,22 @@ GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . """ -from distutils.version import StrictVersion as Version -import queue, mimetypes, platform, os, sys, socket, logging, hmac + +import hmac +import logging +import mimetypes +import os +import queue +import socket +import sys +import tempfile +from distutils.version import LooseVersion as Version from urllib.request import urlopen -from flask import Flask, Response, request, render_template_string, abort, make_response -from flask import __version__ as flask_version +from flask import ( + Flask, Response, request, render_template_string, abort, make_response, + __version__ as flask_version +) from . import strings, common @@ -56,6 +66,7 @@ security_headers = [ ('Server', 'OnionShare') ] + def set_file_info(filenames, processed_size_callback=None): """ Using the list of filenames being shared, fill in details that the web @@ -115,6 +126,8 @@ def add_request(request_type, path, data=None): slug = None + + def generate_slug(): global slug slug = common.build_slug() @@ -123,12 +136,16 @@ download_count = 0 error404_count = 0 stay_open = False + + def set_stay_open(new_stay_open): """ Set stay_open variable. """ global stay_open stay_open = new_stay_open + + def get_stay_open(): """ Get stay_open variable. @@ -138,6 +155,8 @@ def get_stay_open(): # Are we running in GUI mode? gui_mode = False + + def set_gui_mode(): """ Tell the web service that we're running in GUI mode @@ -145,21 +164,19 @@ def set_gui_mode(): global gui_mode gui_mode = True + def debug_mode(): """ Turn on debugging mode, which will log flask errors to a debug file. """ - if platform.system() == 'Windows': - temp_dir = os.environ['Temp'].replace('\\', '/') - else: - temp_dir = '/tmp/' - - log_handler = logging.FileHandler('{0:s}/onionshare_server.log'.format(temp_dir)) + temp_dir = tempfile.gettempdir() + log_handler = logging.FileHandler( + os.path.join(temp_dir, 'onionshare_server.log')) log_handler.setLevel(logging.WARNING) app.logger.addHandler(log_handler) -def check_slug_candidate(slug_candidate, slug_compare = None): - global slug + +def check_slug_candidate(slug_candidate, slug_compare=None): if not slug_compare: slug_compare = slug if not hmac.compare_digest(slug_compare, slug_candidate): @@ -170,6 +187,7 @@ def check_slug_candidate(slug_candidate, slug_compare = None): # one download at a time. download_in_progress = False + @app.route("/") def index(slug_candidate): """ @@ -185,7 +203,7 @@ def index(slug_candidate): deny_download = not stay_open and download_in_progress if deny_download: r = make_response(render_template_string(open(common.get_resource_path('html/denied.html')).read())) - for header,value in security_headers: + for header, value in security_headers: r.headers.set(header, value) return r @@ -198,15 +216,17 @@ def index(slug_candidate): filename=os.path.basename(zip_filename), filesize=zip_filesize, filesize_human=common.human_readable_filesize(zip_filesize))) - for header,value in security_headers: + for header, value in security_headers: r.headers.set(header, value) return r + # If the client closes the OnionShare window while a download is in progress, # it should immediately stop serving the file. The client_cancel global is # used to tell the download function that the client is canceling the download. client_cancel = False + @app.route("//download") def download(slug_candidate): """ @@ -331,11 +351,12 @@ def page_not_found(e): force_shutdown() print(strings._('error_rate_limit')) - r = make_response(render_template_string(open(common.get_resource_path('html/404.html')).read())) - for header,value in security_headers: + r = make_response(render_template_string(open(common.get_resource_path('html/404.html')).read()), 404) + for header, value in security_headers: r.headers.set(header, value) return r + # shutting down the server only works within the context of flask, so the easiest way to do it is over http shutdown_slug = common.random_string(16) diff --git a/onionshare_gui/onionshare_gui.py b/onionshare_gui/onionshare_gui.py index bdf0d105..2a3b3468 100644 --- a/onionshare_gui/onionshare_gui.py +++ b/onionshare_gui/onionshare_gui.py @@ -434,6 +434,9 @@ class OnionShareGui(QtWidgets.QMainWindow): if not web.get_stay_open(): self.server_status.stop_server() self.server_status.shutdown_timeout_reset() + else: + if self.server_status.status == self.server_status.STATUS_STOPPED: + self.downloads.cancel_download(event["data"]["id"]) elif event["type"] == web.REQUEST_CANCELED: download_in_progress = False diff --git a/onionshare_gui/tor_connection_dialog.py b/onionshare_gui/tor_connection_dialog.py index 93c03bef..fa4c7860 100644 --- a/onionshare_gui/tor_connection_dialog.py +++ b/onionshare_gui/tor_connection_dialog.py @@ -125,7 +125,7 @@ class TorConnectionThread(QtCore.QThread): # Connect to the Onion try: - self.onion.connect(self.settings, self._tor_status_update) + self.onion.connect(self.settings, False, self._tor_status_update) if self.onion.connected_to_tor: self.connected_to_tor.emit() else: diff --git a/share/locale/en.json b/share/locale/en.json index 03a6a28e..ff288b6a 100644 --- a/share/locale/en.json +++ b/share/locale/en.json @@ -118,5 +118,6 @@ "gui_tor_connection_ask_quit": "Quit", "gui_tor_connection_error_settings": "Try adjusting how OnionShare connects to the Tor network in Settings.", "gui_server_started_after_timeout": "The server started after your chosen auto-timeout.\nPlease start a new share.", - "gui_server_timeout_expired": "The chosen timeout has already expired.\nPlease update the timeout and then you may start sharing." + "gui_server_timeout_expired": "The chosen timeout has already expired.\nPlease update the timeout and then you may start sharing.", + "share_via_onionshare": "Share via OnionShare" } diff --git a/test/onionshare_test.py b/test/test_onionshare.py similarity index 100% rename from test/onionshare_test.py rename to test/test_onionshare.py diff --git a/test/onionshare_common_test.py b/test/test_onionshare_common.py similarity index 100% rename from test/onionshare_common_test.py rename to test/test_onionshare_common.py diff --git a/test/onionshare_settings_test.py b/test/test_onionshare_settings.py similarity index 100% rename from test/onionshare_settings_test.py rename to test/test_onionshare_settings.py diff --git a/test/onionshare_strings_test.py b/test/test_onionshare_strings.py similarity index 100% rename from test/onionshare_strings_test.py rename to test/test_onionshare_strings.py