Merge branch 'develop'

This commit is contained in:
Micah Lee 2018-02-25 17:45:27 -08:00
commit f7dba635eb
No known key found for this signature in database
GPG Key ID: 403C2657CD994F73
54 changed files with 1258 additions and 419 deletions

1
.gitignore vendored
View File

@ -28,6 +28,7 @@ pip-log.txt
.tox
nosetests.xml
.cache
.pytest_cache
# Translations
*.mo

View File

@ -10,7 +10,12 @@ python:
- "nightly"
# command to install dependencies
install:
- pip install Flask==0.12 stem==1.5.4 pytest-cov coveralls
- pip install Flask==0.12 stem==1.5.4 pytest-cov coveralls flake8
before_script:
# stop the build if there are Python syntax errors or undefined names
- flake8 . --count --select=E901,E999,F821,F822,F823 --show-source --statistics
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
- flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
# command to run tests
script: pytest --cov=onionshare test/
after_success:

View File

@ -38,6 +38,8 @@ Install Xcode from the Mac App Store. Once it's installed, run it for the first
Download and install Python 3.6.4 from https://www.python.org/downloads/release/python-364/. I downloaded `python-3.6.4-macosx10.6.pkg`.
You may also need to run the command `/Applications/Python\ 3.6/Install\ Certificates.command` to update Python 3.6's internal certificate store. Otherwise, you may find that fetching the Tor Browser .dmg file fails later due to a certificate validation error.
Download and install Qt5 from https://www.qt.io/download-open-source/. I downloaded `qt-unified-mac-x64-3.0.2-online.dmg`. There's no need to login to a Qt account during installation. Make sure you install the latest Qt 5.x. I installed Qt 5.10.0 -- all you need is to check `Qt > Qt 5.10.0 > macOS`.
Now install some python dependencies with pip (note, there's issues building a .app if you install this in a virtualenv):

View File

@ -1,5 +1,20 @@
# OnionShare Changelog
## 1.3
* Major UI redesign, introducing many UX improvements
* Client-side web interfact redesigned
* New feature: Support for meek_lite pluggable transports (Amazon and Azure)
* New feature: Support for custom obfs4 and meek-lite bridges
* New feature: ability to cancel share before it starts
* Bug fix: the UpdateChecker no longer blocks the UI when checking
* Bug fix: simultaneous downloads (broken in 1.2)
* Update Tor to 0.2.3.9
* Improved support for BSD
* Updated French and Danish translations
* Minor build script and build documentation fixes
* Add flake8 tests
## 1.2
* New feature: Support for Tor bridges, including obfs4proxy

View File

@ -14,7 +14,7 @@ You can download OnionShare for Windows and macOS from the [OnionShare website](
## Developing OnionShare
You can set up your development environment to build OnionShare yourself by following [these instructions](/BUILD.md).
You can set up your development environment to build OnionShare yourself by following [these instructions](/BUILD.md). You may also subscribe to our developers mailing list [here](https://lists.riseup.net/www/info/onionshare-dev).
# Screenshots

View File

@ -28,9 +28,9 @@ import inspect, os, sys, hashlib, zipfile, io, shutil, subprocess
import urllib.request
def main():
dmg_url = 'https://archive.torproject.org/tor-package-archive/torbrowser/7.0.11/TorBrowser-7.0.11-osx64_en-US.dmg'
dmg_filename = 'TorBrowser-7.0.11-osx64_en-US.dmg'
expected_dmg_sha256 = '5143e4a2141a69f66869be13eef4bcaac4e6c27c78383fc8a4c38b334759f3a2'
dmg_url = 'https://archive.torproject.org/tor-package-archive/torbrowser/7.5/TorBrowser-7.5-osx64_en-US.dmg'
dmg_filename = 'TorBrowser-7.5-osx64_en-US.dmg'
expected_dmg_sha256 = '43a8dc0afd0a77e42766311eb54ad9fc8714f67fcd2d3582a3bcb98b22c2e629'
# Build paths
root_path = os.path.dirname(os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe()))))

View File

@ -28,9 +28,9 @@ import inspect, os, sys, hashlib, shutil, subprocess
import urllib.request
def main():
exe_url = 'https://archive.torproject.org/tor-package-archive/torbrowser/7.0.11/torbrowser-install-7.0.11_en-US.exe'
exe_filename = 'torbrowser-install-7.0.11_en-US.exe'
expected_exe_sha256 = 'a033eb9b9ed2ad389169b36a90946a8af8f05bd0c7bbd3e37678041331096624'
exe_url = 'https://archive.torproject.org/tor-package-archive/torbrowser/7.5/torbrowser-install-7.5_en-US.exe'
exe_filename = 'torbrowser-install-7.5_en-US.exe'
expected_exe_sha256 = '81ccb9456118cf8fa755a3eafb5c514665fc69599cdd41e9eb36baa335ebe233'
# Build paths
root_path = os.path.dirname(os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe()))))
working_path = os.path.join(os.path.join(root_path, 'build'), 'tor')

View File

@ -1,9 +1,11 @@
[Desktop Entry]
Name=OnionShare
Comment=Share a file securely and anonymously over Tor
Comment[da]=Del en fil sikkert og anonymt over Tor
Exec=/usr/bin/onionshare-gui
Terminal=false
Type=Application
Icon=/usr/share/pixmaps/onionshare80.xpm
Categories=Network;
Keywords=tor;anonymity;privacy;onion service;file sharing;file hosting;
Keywords[da]=tor;anonymitet;privatliv;onion-tjeneste;fildeling;filhosting;

View File

@ -3,10 +3,10 @@
!define ABOUTURL "https:\\onionshare.org\"
# change these with each release
!define INSTALLSIZE 66525
!define INSTALLSIZE 66537
!define VERSIONMAJOR 1
!define VERSIONMINOR 2
!define VERSIONSTRING "1.2"
!define VERSIONMINOR 3
!define VERSIONSTRING "1.3"
RequestExecutionLevel admin
@ -192,6 +192,8 @@ Section "install"
File "${BINPATH}\share\torrc_template"
File "${BINPATH}\share\torrc_template-windows"
File "${BINPATH}\share\torrc_template-obfs4"
File "${BINPATH}\share\torrc_template-meek_lite_amazon"
File "${BINPATH}\share\torrc_template-meek_lite_azure"
File "${BINPATH}\share\version.txt"
File "${BINPATH}\share\wordlist.txt"
@ -201,14 +203,22 @@ Section "install"
File "${BINPATH}\share\html\index.html"
SetOutPath "$INSTDIR\share\images"
File "${BINPATH}\share\images\drop_files.png"
File "${BINPATH}\share\images\download_completed.png"
File "${BINPATH}\share\images\download_completed_none.png"
File "${BINPATH}\share\images\download_in_progress.png"
File "${BINPATH}\share\images\download_in_progress_none.png"
File "${BINPATH}\share\images\favicon.ico"
File "${BINPATH}\share\images\file_delete.png"
File "${BINPATH}\share\images\info.png"
File "${BINPATH}\share\images\logo.png"
File "${BINPATH}\share\images\logo_transparent.png"
File "${BINPATH}\share\images\logo_grayscale.png"
File "${BINPATH}\share\images\server_started.png"
File "${BINPATH}\share\images\server_stopped.png"
File "${BINPATH}\share\images\server_working.png"
File "${BINPATH}\share\images\settings.png"
File "${BINPATH}\share\images\settings_inactive.png"
File "${BINPATH}\share\images\web_file.png"
File "${BINPATH}\share\images\web_folder.png"
SetOutPath "$INSTDIR\share\locale"
File "${BINPATH}\share\locale\cs.json"
@ -379,14 +389,22 @@ FunctionEnd
Delete "$INSTDIR\share\html\404.html"
Delete "$INSTDIR\share\html\denied.html"
Delete "$INSTDIR\share\html\index.html"
Delete "$INSTDIR\share\images\drop_files.png"
Delete "$INSTDIR\share\images\download_completed.png"
Delete "$INSTDIR\share\images\download_completed_none.png"
Delete "$INSTDIR\share\images\download_in_progress.png"
Delete "$INSTDIR\share\images\download_in_progress_none.png"
Delete "$INSTDIR\share\images\favicon.ico"
Delete "$INSTDIR\share\images\file_delete.png"
Delete "$INSTDIR\share\images\info.png"
Delete "$INSTDIR\share\images\logo.png"
Delete "$INSTDIR\share\images\logo_transparent.png"
Delete "$INSTDIR\share\images\logo_grayscale.png"
Delete "$INSTDIR\share\images\server_started.png"
Delete "$INSTDIR\share\images\server_stopped.png"
Delete "$INSTDIR\share\images\server_working.png"
Delete "$INSTDIR\share\images\settings.png"
Delete "$INSTDIR\share\images\settings_inactive.png"
Delete "$INSTDIR\share\images\web_file.png"
Delete "$INSTDIR\share\images\web_folder.png"
Delete "$INSTDIR\share\license.txt"
Delete "$INSTDIR\share\locale\cs.json"
Delete "$INSTDIR\share\locale\de.json"
@ -404,6 +422,8 @@ FunctionEnd
Delete "$INSTDIR\share\torrc_template"
Delete "$INSTDIR\share\torrc_template-windows"
Delete "$INSTDIR\share\torrc_template-obfs4"
Delete "$INSTDIR\share\torrc_template-meek_lite_amazon"
Delete "$INSTDIR\share\torrc_template-meek_lite_azure"
Delete "$INSTDIR\share\version.txt"
Delete "$INSTDIR\share\wordlist.txt"
Delete "$INSTDIR\sip.pyd"

View File

@ -15,6 +15,8 @@ a = Analysis(
('../share/wordlist.txt', 'share'),
('../share/torrc_template', 'share'),
('../share/torrc_template-obfs4', 'share'),
('../share/torrc_template-meek_lite_amazon', 'share'),
('../share/torrc_template-meek_lite_azure', 'share'),
('../share/torrc_template-windows', 'share'),
('../share/images/*', 'share/images'),
('../share/locale/*', 'share/locale'),

View File

@ -64,7 +64,7 @@ class OnionShareExtension(GObject.GObject, Nautilus.MenuProvider):
"""
def url2path(self,url):
file_uri = url.get_activation_uri()
file_uri = url.get_activation_uri()
arg_uri = file_uri[7:]
path = urllib.url2pathname(arg_uri)
return path

View File

@ -56,7 +56,10 @@ def get_platform():
"""
Returns the platform OnionShare is running on.
"""
return platform.system()
plat = platform.system()
if plat.endswith('BSD'):
plat = 'BSD'
return plat
def get_resource_path(filename):
@ -66,6 +69,10 @@ def get_resource_path(filename):
"""
p = get_platform()
# On Windows, and in Windows dev mode, switch slashes in incoming filename to backslackes
if p == 'Windows':
filename = filename.replace('/', '\\')
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')
@ -73,7 +80,7 @@ def get_resource_path(filename):
# 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':
elif p == 'BSD' or 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')
@ -107,7 +114,7 @@ def get_tor_paths():
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 p == 'OpenBSD' or p == 'FreeBSD':
elif p == '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'

View File

@ -131,7 +131,7 @@ class Onion(object):
self.stealth = False
self.service_id = None
self.system = platform.system()
self.system = common.get_platform()
# Is bundled tor supported?
if (self.system == 'Windows' or self.system == 'Darwin') and getattr(sys, 'onionshare_dev_mode', False):
@ -183,7 +183,7 @@ class Onion(object):
raise OSError(strings._('no_available_port'))
self.tor_torrc = os.path.join(self.tor_data_directory.name, 'torrc')
else:
# Linux and Mac can use unix sockets
# Linux, Mac and BSD can use unix sockets
with open(common.get_resource_path('torrc_template')) as f:
torrc_template = f.read()
self.tor_control_port = None
@ -211,7 +211,22 @@ class Onion(object):
with open(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_amazon'):
f.write('ClientTransportPlugin meek_lite exec {}\n'.format(self.obfs4proxy_file_path))
with open(common.get_resource_path('torrc_template-meek_lite_amazon')) 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(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')
@ -265,7 +280,10 @@ class Onion(object):
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'):
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_amazon') or \
self.settings.get('tor_bridges_use_meek_lite_azure'):
connect_timeout = 150
else:
# Timeout after 120 seconds
@ -316,7 +334,7 @@ class Onion(object):
# guessing the socket file name next
if not found_tor:
try:
if self.system == 'Linux':
if self.system == 'Linux' or self.system == 'BSD':
socket_file_path = '/run/user/{}/Tor/control.socket'.format(os.geteuid())
elif self.system == 'Darwin':
socket_file_path = '/run/user/{}/Tor/control.socket'.format(os.geteuid())
@ -470,8 +488,8 @@ class Onion(object):
auth_cookie = list(res.client_auth.values())[0]
self.auth_string = 'HidServAuth {} {}'.format(onion_host, auth_cookie)
self.settings.save()
if onion_host is not None:
self.settings.save()
return onion_host
else:
raise TorErrorProtocolError(strings._('error_tor_protocol_error'))
@ -482,13 +500,19 @@ class Onion(object):
"""
common.log('Onion', 'cleanup')
# Cleanup the ephemeral onion service
if self.service_id:
try:
self.c.remove_ephemeral_hidden_service(self.service_id)
except:
pass
self.service_id = None
# Cleanup the ephemeral onion services, if we have any
try:
onions = self.c.list_ephemeral_hidden_services()
for onion in onions:
try:
common.log('Onion', 'cleanup', 'trying to remove onion {}'.format(onion))
self.c.remove_ephemeral_hidden_service(onion)
except:
common.log('Onion', 'cleanup', 'could not remove onion {}.. moving on anyway'.format(onion))
pass
except:
pass
self.service_id = None
if stop_tor:
# Stop tor process

View File

@ -58,11 +58,14 @@ class Settings(object):
'auth_password': '',
'close_after_first_download': True,
'systray_notifications': True,
'shutdown_timeout': False,
'use_stealth': False,
'use_autoupdate': True,
'autoupdate_timestamp': None,
'no_bridges': True,
'tor_bridges_use_obfs4': False,
'tor_bridges_use_meek_lite_amazon': False,
'tor_bridges_use_meek_lite_azure': False,
'tor_bridges_use_custom_bridges': '',
'save_private_key': False,
'private_key': '',

View File

@ -26,6 +26,7 @@ import queue
import socket
import sys
import tempfile
import base64
from distutils.version import LooseVersion as Version
from urllib.request import urlopen
@ -58,7 +59,7 @@ zip_filename = None
zip_filesize = None
security_headers = [
('Content-Security-Policy', 'default-src \'self\'; style-src \'unsafe-inline\'; img-src \'self\' data:;'),
('Content-Security-Policy', 'default-src \'self\'; style-src \'unsafe-inline\'; script-src \'unsafe-inline\'; img-src \'self\' data:;'),
('X-Frame-Options', 'DENY'),
('X-Xss-Protection', '1; mode=block'),
('X-Content-Type-Options', 'nosniff'),
@ -125,6 +126,12 @@ def add_request(request_type, path, data=None):
})
# Load and base64 encode images to pass into templates
favicon_b64 = base64.b64encode(open(common.get_resource_path('images/favicon.ico'), 'rb').read()).decode()
logo_b64 = base64.b64encode(open(common.get_resource_path('images/logo.png'), 'rb').read()).decode()
folder_b64 = base64.b64encode(open(common.get_resource_path('images/web_folder.png'), 'rb').read()).decode()
file_b64 = base64.b64encode(open(common.get_resource_path('images/web_file.png'), 'rb').read()).decode()
slug = None
@ -206,7 +213,10 @@ def index(slug_candidate):
global stay_open, download_in_progress
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()))
r = make_response(render_template_string(
open(common.get_resource_path('html/denied.html')).read(),
favicon_b64=favicon_b64
))
for header, value in security_headers:
r.headers.set(header, value)
return r
@ -215,6 +225,10 @@ def index(slug_candidate):
r = make_response(render_template_string(
open(common.get_resource_path('html/index.html')).read(),
favicon_b64=favicon_b64,
logo_b64=logo_b64,
folder_b64=folder_b64,
file_b64=file_b64,
slug=slug,
file_info=file_info,
filename=os.path.basename(zip_filename),
@ -243,7 +257,10 @@ def download(slug_candidate):
global stay_open, download_in_progress, done
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()))
r = make_response(render_template_string(
open(common.get_resource_path('html/denied.html')).read(),
favicon_b64=favicon_b64
))
for header,value in security_headers:
r.headers.set(header, value)
return r
@ -298,12 +315,14 @@ def download(slug_candidate):
percent = (1.0 * downloaded_bytes / zip_filesize) * 100
# only output to stdout if running onionshare in CLI mode, or if using Linux (#203, #304)
if not gui_mode or common.get_platform() == 'Linux':
plat = common.get_platform()
if not gui_mode or plat == 'Linux' or plat == 'BSD':
sys.stdout.write(
"\r{0:s}, {1:.2f}% ".format(common.human_readable_filesize(downloaded_bytes), percent))
sys.stdout.flush()
add_request(REQUEST_PROGRESS, path, {'id': download_id, 'bytes': downloaded_bytes})
done = False
except:
# looks like the download was canceled
done = True
@ -355,7 +374,10 @@ 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()), 404)
r = make_response(render_template_string(
open(common.get_resource_path('html/404.html')).read(),
favicon_b64=favicon_b64
), 404)
for header, value in security_headers:
r.headers.set(header, value)
return r

View File

@ -35,8 +35,8 @@ class Application(QtWidgets.QApplication):
and the quick keyboard shortcut.
"""
def __init__(self):
system = platform.system()
if system == 'Linux':
system = common.get_platform()
if system == 'Linux' or system == 'BSD':
self.setAttribute(QtCore.Qt.AA_X11InitThreads, True)
QtWidgets.QApplication.__init__(self, sys.argv)
self.installEventFilter(self)

View File

@ -34,13 +34,15 @@ class Download(object):
# make a new progress bar
cssStyleData ="""
QProgressBar {
border: 2px solid grey;
border-radius: 5px;
border: 1px solid #4e064f;
background-color: #ffffff !important;
text-align: center;
color: #9b9b9b;
font-size: 12px;
}
QProgressBar::chunk {
background: qlineargradient(x1: 0.5, y1: 0, x2: 0.5, y2: 1, stop: 0 #b366ff, stop: 1 #d9b3ff);
background-color: #4e064f;
width: 10px;
}"""
self.progress_bar = QtWidgets.QProgressBar()

View File

@ -23,6 +23,50 @@ from .alert import Alert
from onionshare import strings, common
class DropHereLabel(QtWidgets.QLabel):
"""
When there are no files or folders in the FileList yet, display the
'drop files here' message and graphic.
"""
def __init__(self, parent, image=False):
self.parent = parent
super(DropHereLabel, self).__init__(parent=parent)
self.setAcceptDrops(True)
self.setAlignment(QtCore.Qt.AlignCenter)
if image:
self.setPixmap(QtGui.QPixmap.fromImage(QtGui.QImage(common.get_resource_path('images/logo_transparent.png'))))
else:
self.setText(strings._('gui_drag_and_drop', True))
self.setStyleSheet('color: #999999;')
self.hide()
def dragEnterEvent(self, event):
self.parent.drop_here_image.hide()
self.parent.drop_here_text.hide()
event.accept()
class DropCountLabel(QtWidgets.QLabel):
"""
While dragging files over the FileList, this counter displays the
number of files you're dragging.
"""
def __init__(self, parent):
self.parent = parent
super(DropCountLabel, self).__init__(parent=parent)
self.setAcceptDrops(True)
self.setAlignment(QtCore.Qt.AlignCenter)
self.setText(strings._('gui_drag_and_drop', True))
self.setStyleSheet('color: #ffffff; background-color: #f44449; font-weight: bold; padding: 5px 10px; border-radius: 10px;')
self.hide()
def dragEnterEvent(self, event):
self.hide()
event.accept()
class FileList(QtWidgets.QListWidget):
"""
The list of files and folders in the GUI.
@ -35,63 +79,82 @@ class FileList(QtWidgets.QListWidget):
self.setAcceptDrops(True)
self.setIconSize(QtCore.QSize(32, 32))
self.setSortingEnabled(True)
self.setMinimumHeight(200)
self.setMinimumHeight(205)
self.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
class DropHereLabel(QtWidgets.QLabel):
"""
When there are no files or folders in the FileList yet, display the
'drop files here' message and graphic.
"""
def __init__(self, parent, image=False):
self.parent = parent
super(DropHereLabel, self).__init__(parent=parent)
self.setAcceptDrops(True)
self.setAlignment(QtCore.Qt.AlignCenter)
if image:
self.setPixmap(QtGui.QPixmap.fromImage(QtGui.QImage(common.get_resource_path('images/drop_files.png'))))
else:
self.setText(strings._('gui_drag_and_drop', True))
self.setStyleSheet('color: #999999;')
self.hide()
def dragEnterEvent(self, event):
self.parent.drop_here_image.hide()
self.parent.drop_here_text.hide()
event.ignore()
self.drop_here_image = DropHereLabel(self, True)
self.drop_here_text = DropHereLabel(self, False)
self.filenames = []
self.update()
self.drop_count = DropCountLabel(self)
self.resizeEvent(None)
self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
def update(self):
"""
Update the GUI elements based on the current state.
"""
# file list should have a background image if empty
if len(self.filenames) == 0:
if self.count() == 0:
self.drop_here_image.show()
self.drop_here_text.show()
else:
self.drop_here_image.hide()
self.drop_here_text.hide()
def server_started(self):
"""
Update the GUI when the server starts, by hiding delete buttons.
"""
self.setAcceptDrops(False)
self.setCurrentItem(None)
self.setSelectionMode(QtWidgets.QAbstractItemView.NoSelection)
for index in range(self.count()):
self.item(index).item_button.hide()
def server_stopped(self):
"""
Update the GUI when the server stops, by showing delete buttons.
"""
self.setAcceptDrops(True)
self.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
for index in range(self.count()):
self.item(index).item_button.show()
def resizeEvent(self, event):
"""
When the widget is resized, resize the drop files image and text.
"""
self.drop_here_image.setGeometry(0, 0, self.width(), self.height())
self.drop_here_text.setGeometry(0, 0, self.width(), self.height())
offset = 70
self.drop_here_image.setGeometry(0, 0, self.width(), self.height() - offset)
self.drop_here_text.setGeometry(0, offset, self.width(), self.height() - offset)
if self.count() > 0:
# Add and delete an empty item, to force all items to get redrawn
# This is ugly, but the only way I could figure out how to proceed
item = QtWidgets.QListWidgetItem('fake item')
self.addItem(item)
self.takeItem(self.row(item))
self.update()
# Extend any filenames that were truncated to fit the window
# We use 200 as a rough guess at how wide the 'file size + delete button' widget is
# and extend based on the overall width minus that amount.
for index in range(self.count()):
metrics = QtGui.QFontMetrics(self.item(index).font())
elided = metrics.elidedText(self.item(index).basename, QtCore.Qt.ElideRight, self.width() - 200)
self.item(index).setText(elided)
def dragEnterEvent(self, event):
"""
dragEnterEvent for dragging files and directories into the widget.
"""
if event.mimeData().hasUrls:
self.setStyleSheet('FileList { border: 3px solid #538ad0; }')
count = len(event.mimeData().urls())
self.drop_count.setText('+{}'.format(count))
size_hint = self.drop_count.sizeHint()
self.drop_count.setGeometry(self.width() - size_hint.width() - 10, self.height() - size_hint.height() - 10, size_hint.width(), size_hint.height())
self.drop_count.show()
event.accept()
else:
event.ignore()
@ -100,6 +163,8 @@ class FileList(QtWidgets.QListWidget):
"""
dragLeaveEvent for dragging files and directories into the widget.
"""
self.setStyleSheet('FileList { border: none; }')
self.drop_count.hide()
event.accept()
self.update()
@ -125,36 +190,84 @@ class FileList(QtWidgets.QListWidget):
self.add_file(filename)
else:
event.ignore()
self.setStyleSheet('border: none;')
self.drop_count.hide()
self.files_dropped.emit()
def add_file(self, filename):
"""
Add a file or directory to this widget.
"""
if filename not in self.filenames:
filenames = []
for index in range(self.count()):
filenames.append(self.item(index).filename)
if filename not in filenames:
if not os.access(filename, os.R_OK):
Alert(strings._("not_a_readable_file", True).format(filename))
return
self.filenames.append(filename)
# Re-sort the list internally
self.filenames.sort()
fileinfo = QtCore.QFileInfo(filename)
basename = os.path.basename(filename.rstrip('/'))
ip = QtWidgets.QFileIconProvider()
icon = ip.icon(fileinfo)
if os.path.isfile(filename):
size = common.human_readable_filesize(fileinfo.size())
size_bytes = fileinfo.size()
size_readable = common.human_readable_filesize(size_bytes)
else:
size = common.human_readable_filesize(common.dir_size(filename))
item_name = '{0:s} ({1:s})'.format(basename, size)
item = QtWidgets.QListWidgetItem(item_name)
item.setToolTip(size)
size_bytes = common.dir_size(filename)
size_readable = common.human_readable_filesize(size_bytes)
# Create a new item
item = QtWidgets.QListWidgetItem()
item.setIcon(icon)
item.size_bytes = size_bytes
# Item's filename attribute and size labels
item.filename = filename
item_size = QtWidgets.QLabel(size_readable)
item_size.setStyleSheet('QLabel { color: #666666; font-size: 11px; }')
item.basename = os.path.basename(filename.rstrip('/'))
# Use the basename as the method with which to sort the list
metrics = QtGui.QFontMetrics(item.font())
elided = metrics.elidedText(item.basename, QtCore.Qt.ElideRight, self.sizeHint().width())
item.setData(QtCore.Qt.DisplayRole, elided)
# Item's delete button
def delete_item():
itemrow = self.row(item)
self.takeItem(itemrow)
self.files_updated.emit()
item.item_button = QtWidgets.QPushButton()
item.item_button.setDefault(False)
item.item_button.setFlat(True)
item.item_button.setIcon( QtGui.QIcon(common.get_resource_path('images/file_delete.png')) )
item.item_button.clicked.connect(delete_item)
item.item_button.setSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed)
# Item info widget, with a white background
item_info_layout = QtWidgets.QHBoxLayout()
item_info_layout.addWidget(item_size)
item_info_layout.addWidget(item.item_button)
item_info = QtWidgets.QWidget()
item_info.setObjectName('item-info')
item_info.setLayout(item_info_layout)
# Create the item's widget and layouts
item_hlayout = QtWidgets.QHBoxLayout()
item_hlayout.addStretch()
item_hlayout.addWidget(item_info)
widget = QtWidgets.QWidget()
widget.setLayout(item_hlayout)
item.setSizeHint(widget.sizeHint())
self.addItem(item)
self.setItemWidget(item, widget)
self.files_updated.emit()
@ -168,21 +281,23 @@ class FileSelection(QtWidgets.QVBoxLayout):
super(FileSelection, self).__init__()
self.server_on = False
# file list
# File list
self.file_list = FileList()
self.file_list.currentItemChanged.connect(self.update)
self.file_list.itemSelectionChanged.connect(self.update)
self.file_list.files_dropped.connect(self.update)
self.file_list.files_updated.connect(self.update)
# buttons
# Buttons
self.add_button = QtWidgets.QPushButton(strings._('gui_add', True))
self.add_button.clicked.connect(self.add)
self.delete_button = QtWidgets.QPushButton(strings._('gui_delete', True))
self.delete_button.clicked.connect(self.delete)
button_layout = QtWidgets.QHBoxLayout()
button_layout.addStretch()
button_layout.addWidget(self.add_button)
button_layout.addWidget(self.delete_button)
# add the widgets
# Add the widgets
self.addWidget(self.file_list)
self.addLayout(button_layout)
@ -192,21 +307,20 @@ class FileSelection(QtWidgets.QVBoxLayout):
"""
Update the GUI elements based on the current state.
"""
# all buttons should be disabled if the server is on
# All buttons should be hidden if the server is on
if self.server_on:
self.add_button.setEnabled(False)
self.delete_button.setEnabled(False)
self.add_button.hide()
self.delete_button.hide()
else:
self.add_button.setEnabled(True)
self.add_button.show()
# delete button should be disabled if item isn't selected
current_item = self.file_list.currentItem()
if not current_item:
self.delete_button.setEnabled(False)
# Delete button should be hidden if item isn't selected
if len(self.file_list.selectedItems()) == 0:
self.delete_button.hide()
else:
self.delete_button.setEnabled(True)
self.delete_button.show()
# update the file list
# Update the file list
self.file_list.update()
def add(self):
@ -218,6 +332,7 @@ class FileSelection(QtWidgets.QVBoxLayout):
for filename in file_dialog.selectedFiles():
self.file_list.add_file(filename)
self.file_list.setCurrentItem(None)
self.update()
def delete(self):
@ -227,9 +342,10 @@ class FileSelection(QtWidgets.QVBoxLayout):
selected = self.file_list.selectedItems()
for item in selected:
itemrow = self.file_list.row(item)
self.file_list.filenames.pop(itemrow)
self.file_list.takeItem(itemrow)
self.file_list.files_updated.emit()
self.file_list.setCurrentItem(None)
self.update()
def server_started(self):
@ -237,7 +353,7 @@ class FileSelection(QtWidgets.QVBoxLayout):
Gets called when the server starts.
"""
self.server_on = True
self.file_list.setAcceptDrops(False)
self.file_list.server_started()
self.update()
def server_stopped(self):
@ -245,14 +361,14 @@ class FileSelection(QtWidgets.QVBoxLayout):
Gets called when the server stops.
"""
self.server_on = False
self.file_list.setAcceptDrops(True)
self.file_list.server_stopped()
self.update()
def get_num_files(self):
"""
Returns the total number of files and folders in the list.
"""
return len(self.file_list.filenames)
return len(range(self.file_list.count()))
def setFocus(self):
"""

View File

@ -56,6 +56,7 @@ class OnionShareGui(QtWidgets.QMainWindow):
self.setWindowTitle('OnionShare')
self.setWindowIcon(QtGui.QIcon(common.get_resource_path('images/logo.png')))
self.setMinimumWidth(430)
# Load settings
self.config = config
@ -72,20 +73,31 @@ class OnionShareGui(QtWidgets.QMainWindow):
self.server_status = ServerStatus(self.qtapp, self.app, web, self.file_selection, self.settings)
self.server_status.server_started.connect(self.file_selection.server_started)
self.server_status.server_started.connect(self.start_server)
self.server_status.server_started.connect(self.update_server_status_indicator)
self.server_status.server_stopped.connect(self.file_selection.server_stopped)
self.server_status.server_stopped.connect(self.stop_server)
self.server_status.server_stopped.connect(self.update_server_status_indicator)
self.server_status.server_stopped.connect(self.update_primary_action)
self.server_status.server_canceled.connect(self.cancel_server)
self.server_status.server_canceled.connect(self.file_selection.server_stopped)
self.server_status.server_canceled.connect(self.update_primary_action)
self.start_server_finished.connect(self.clear_message)
self.start_server_finished.connect(self.server_status.start_server_finished)
self.start_server_finished.connect(self.update_server_status_indicator)
self.stop_server_finished.connect(self.server_status.stop_server_finished)
self.stop_server_finished.connect(self.update_server_status_indicator)
self.file_selection.file_list.files_updated.connect(self.server_status.update)
self.file_selection.file_list.files_updated.connect(self.update_primary_action)
self.server_status.url_copied.connect(self.copy_url)
self.server_status.hidservauth_copied.connect(self.copy_hidservauth)
self.starting_server_step2.connect(self.start_server_step2)
self.starting_server_step3.connect(self.start_server_step3)
self.starting_server_error.connect(self.start_server_error)
self.server_status.button_clicked.connect(self.clear_message)
# Filesize warning
self.filesize_warning = QtWidgets.QLabel()
self.filesize_warning.setWordWrap(True)
self.filesize_warning.setStyleSheet('padding: 10px 0; font-weight: bold; color: #333333;')
self.filesize_warning.hide()
@ -99,38 +111,95 @@ class OnionShareGui(QtWidgets.QMainWindow):
self.vbar = self.downloads_container.verticalScrollBar()
self.downloads_container.hide() # downloads start out hidden
self.new_download = False
self.downloads_in_progress = 0
self.downloads_completed = 0
# Info label along top of screen
self.info_layout = QtWidgets.QHBoxLayout()
self.info_label = QtWidgets.QLabel()
self.info_label.setStyleSheet('QLabel { font-size: 12px; color: #666666; }')
self.info_in_progress_downloads_count = QtWidgets.QLabel()
self.info_in_progress_downloads_count.setStyleSheet('QLabel { font-size: 12px; color: #666666; }')
self.info_completed_downloads_count = QtWidgets.QLabel()
self.info_completed_downloads_count.setStyleSheet('QLabel { font-size: 12px; color: #666666; }')
self.update_downloads_completed(self.downloads_in_progress)
self.update_downloads_in_progress(self.downloads_in_progress)
self.info_layout.addWidget(self.info_label)
self.info_layout.addStretch()
self.info_layout.addWidget(self.info_in_progress_downloads_count)
self.info_layout.addWidget(self.info_completed_downloads_count)
self.info_widget = QtWidgets.QWidget()
self.info_widget.setLayout(self.info_layout)
self.info_widget.hide()
# Settings button on the status bar
self.settings_button = QtWidgets.QPushButton()
self.settings_button.setDefault(False)
self.settings_button.setFlat(True)
self.settings_button.setFixedWidth(40)
self.settings_button.setIcon( QtGui.QIcon(common.get_resource_path('images/settings.png')) )
self.settings_button.clicked.connect(self.open_settings)
# Server status indicator on the status bar
self.server_status_image_stopped = QtGui.QImage(common.get_resource_path('images/server_stopped.png'))
self.server_status_image_working = QtGui.QImage(common.get_resource_path('images/server_working.png'))
self.server_status_image_started = QtGui.QImage(common.get_resource_path('images/server_started.png'))
self.server_status_image_label = QtWidgets.QLabel()
self.server_status_image_label.setFixedWidth(20)
self.server_status_label = QtWidgets.QLabel()
self.server_status_label.setStyleSheet('QLabel { font-style: italic; color: #666666; }')
server_status_indicator_layout = QtWidgets.QHBoxLayout()
server_status_indicator_layout.addWidget(self.server_status_image_label)
server_status_indicator_layout.addWidget(self.server_status_label)
self.server_status_indicator = QtWidgets.QWidget()
self.server_status_indicator.setLayout(server_status_indicator_layout)
self.update_server_status_indicator()
# Status bar
self.status_bar = QtWidgets.QStatusBar()
self.status_bar.setSizeGripEnabled(False)
self.status_bar.setStyleSheet(
"QStatusBar::item { border: 0px; }")
version_label = QtWidgets.QLabel('v{0:s}'.format(common.get_version()))
version_label.setStyleSheet('color: #666666')
self.settings_button = QtWidgets.QPushButton()
self.settings_button.setDefault(False)
self.settings_button.setFlat(True)
self.settings_button.setIcon( QtGui.QIcon(common.get_resource_path('images/settings.png')) )
self.settings_button.clicked.connect(self.open_settings)
self.status_bar.addPermanentWidget(version_label)
statusBar_cssStyleData ="""
QStatusBar {
font-style: italic;
color: #666666;
}
QStatusBar::item {
border: 0px;
}"""
self.status_bar.setStyleSheet(statusBar_cssStyleData)
self.status_bar.addPermanentWidget(self.server_status_indicator)
self.status_bar.addPermanentWidget(self.settings_button)
self.setStatusBar(self.status_bar)
# Status bar, zip progress bar
self._zip_progress_bar = None
# Status bar, sharing messages
self.server_share_status_label = QtWidgets.QLabel('')
self.server_share_status_label.setStyleSheet('QLabel { font-style: italic; color: #666666; padding: 2px; }')
self.status_bar.insertWidget(0, self.server_share_status_label)
# Persistent URL notification
self.persistent_url_label = QtWidgets.QLabel(strings._('persistent_url_in_use', True))
self.persistent_url_label.setStyleSheet('padding: 10px 0; font-weight: bold; color: #333333;')
self.persistent_url_label.hide()
# Primary action layout
primary_action_layout = QtWidgets.QVBoxLayout()
primary_action_layout.addWidget(self.server_status)
primary_action_layout.addWidget(self.filesize_warning)
primary_action_layout.addWidget(self.downloads_container)
self.primary_action = QtWidgets.QWidget()
self.primary_action.setLayout(primary_action_layout)
self.primary_action.hide()
self.update_primary_action()
# Main layout
self.layout = QtWidgets.QVBoxLayout()
self.layout.addWidget(self.info_widget)
self.layout.addLayout(self.file_selection)
self.layout.addLayout(self.server_status)
self.layout.addWidget(self.filesize_warning)
self.layout.addWidget(self.persistent_url_label)
self.layout.addWidget(self.downloads_container)
self.layout.addWidget(self.primary_action)
central_widget = QtWidgets.QWidget()
central_widget.setLayout(self.layout)
self.setCentralWidget(central_widget)
@ -158,6 +227,46 @@ class OnionShareGui(QtWidgets.QMainWindow):
# After connecting to Tor, check for updates
self.check_for_updates()
def update_primary_action(self):
# Show or hide primary action layout
file_count = self.file_selection.file_list.count()
if file_count > 0:
self.primary_action.show()
self.info_widget.show()
# Update the file count in the info label
total_size_bytes = 0
for index in range(self.file_selection.file_list.count()):
item = self.file_selection.file_list.item(index)
total_size_bytes += item.size_bytes
total_size_readable = common.human_readable_filesize(total_size_bytes)
if file_count > 1:
self.info_label.setText(strings._('gui_file_info', True).format(file_count, total_size_readable))
else:
self.info_label.setText(strings._('gui_file_info_single', True).format(file_count, total_size_readable))
else:
self.primary_action.hide()
self.info_widget.hide()
# Resize window
self.adjustSize()
def update_server_status_indicator(self):
common.log('OnionShareGui', 'update_server_status_indicator')
# Set the status image
if self.server_status.status == self.server_status.STATUS_STOPPED:
self.server_status_image_label.setPixmap(QtGui.QPixmap.fromImage(self.server_status_image_stopped))
self.server_status_label.setText(strings._('gui_status_indicator_stopped', True))
elif self.server_status.status == self.server_status.STATUS_WORKING:
self.server_status_image_label.setPixmap(QtGui.QPixmap.fromImage(self.server_status_image_working))
self.server_status_label.setText(strings._('gui_status_indicator_working', True))
elif self.server_status.status == self.server_status.STATUS_STARTED:
self.server_status_image_label.setPixmap(QtGui.QPixmap.fromImage(self.server_status_image_started))
self.server_status_label.setText(strings._('gui_status_indicator_started', True))
def _initSystemTray(self):
system = common.get_platform()
@ -238,11 +347,17 @@ class OnionShareGui(QtWidgets.QMainWindow):
if self.server_status.file_selection.get_num_files() > 0:
self.server_status.server_button.setEnabled(True)
self.status_bar.clearMessage()
# If we switched off the shutdown timeout setting, ensure the widget is hidden.
if not self.settings.get('shutdown_timeout'):
self.server_status.shutdown_timeout_container.hide()
d = SettingsDialog(self.onion, self.qtapp, self.config)
d.settings_saved.connect(reload_settings)
d.exec_()
# When settings close, refresh the server status UI
self.server_status.update()
def start_server(self):
"""
Start the onionshare server. This uses multiple threads to start the Tor onion
@ -257,7 +372,9 @@ class OnionShareGui(QtWidgets.QMainWindow):
# Hide and reset the downloads if we have previously shared
self.downloads_container.hide()
self.downloads.reset_downloads()
self.reset_info_counters()
self.status_bar.clearMessage()
self.server_share_status_label.setText('')
# Reset web counters
web.download_count = 0
@ -284,9 +401,10 @@ class OnionShareGui(QtWidgets.QMainWindow):
# wait for modules in thread to load, preventing a thread-related cx_Freeze crash
time.sleep(0.2)
t = threading.Thread(target=start_onion_service, kwargs={'self': self})
t.daemon = True
t.start()
common.log('OnionshareGui', 'start_server', 'Starting an onion thread')
self.t = OnionThread(function=start_onion_service, kwargs={'self': self})
self.t.daemon = True
self.t.start()
def start_server_step2(self):
"""
@ -296,8 +414,11 @@ class OnionShareGui(QtWidgets.QMainWindow):
# add progress bar to the status bar, indicating the crunching of files.
self._zip_progress_bar = ZipProgressBar(0)
self._zip_progress_bar.total_files_size = OnionShareGui._compute_total_size(
self.file_selection.file_list.filenames)
self.filenames = []
for index in range(self.file_selection.file_list.count()):
self.filenames.append(self.file_selection.file_list.item(index).filename)
self._zip_progress_bar.total_files_size = OnionShareGui._compute_total_size(self.filenames)
self.status_bar.insertWidget(0, self._zip_progress_bar)
# prepare the files for sending in a new thread
@ -307,7 +428,7 @@ class OnionShareGui(QtWidgets.QMainWindow):
if self._zip_progress_bar != None:
self._zip_progress_bar.update_processed_size_signal.emit(x)
try:
web.set_file_info(self.file_selection.file_list.filenames, processed_size_callback=_set_processed_size)
web.set_file_info(self.filenames, processed_size_callback=_set_processed_size)
self.app.cleanup_filenames.append(web.zip_filename)
self.starting_server_step3.emit()
@ -317,7 +438,6 @@ class OnionShareGui(QtWidgets.QMainWindow):
self.starting_server_error.emit(e.strerror)
return
#self.status_bar.showMessage(strings._('gui_starting_server2', True))
t = threading.Thread(target=finish_starting_server, kwargs={'self': self})
t.daemon = True
t.start()
@ -339,7 +459,7 @@ class OnionShareGui(QtWidgets.QMainWindow):
self.filesize_warning.setText(strings._("large_filesize", True))
self.filesize_warning.show()
if self.server_status.timer_enabled:
if self.settings.get('shutdown_timeout'):
# Convert the date value to seconds between now and then
now = QtCore.QDateTime.currentDateTime()
self.timeout = now.secsTo(self.server_status.timeout)
@ -352,9 +472,6 @@ class OnionShareGui(QtWidgets.QMainWindow):
self.stop_server()
self.start_server_error(strings._('gui_server_started_after_timeout'))
if self.settings.get('save_private_key'):
self.persistent_url_label.show()
def start_server_error(self, error):
"""
If there's an error when trying to start the onion service
@ -370,6 +487,14 @@ class OnionShareGui(QtWidgets.QMainWindow):
self._zip_progress_bar = None
self.status_bar.clearMessage()
def cancel_server(self):
"""
Cancel the server while it is preparing to start
"""
if self.t:
self.t.quit()
self.stop_server()
def stop_server(self):
"""
Stop the onionshare server.
@ -386,10 +511,13 @@ class OnionShareGui(QtWidgets.QMainWindow):
# Remove ephemeral service, but don't disconnect from Tor
self.onion.cleanup(stop_tor=False)
self.filesize_warning.hide()
self.persistent_url_label.hide()
self.stop_server_finished.emit()
self.downloads_in_progress = 0
self.downloads_completed = 0
self.update_downloads_in_progress(0)
self.file_selection.file_list.adjustSize()
self.set_server_active(False)
self.stop_server_finished.emit()
def check_for_updates(self):
"""
@ -455,6 +583,8 @@ class OnionShareGui(QtWidgets.QMainWindow):
self.downloads_container.show() # show the downloads layout
self.downloads.add_download(event["data"]["id"], web.zip_filesize)
self.new_download = True
self.downloads_in_progress += 1
self.update_downloads_in_progress(self.downloads_in_progress)
if self.systemTray.supportsMessages() and self.settings.get('systray_notifications'):
self.systemTray.showMessage(strings._('systray_download_started_title', True), strings._('systray_download_started_message', True))
@ -469,16 +599,30 @@ class OnionShareGui(QtWidgets.QMainWindow):
if event["data"]["bytes"] == web.zip_filesize:
if self.systemTray.supportsMessages() and self.settings.get('systray_notifications'):
self.systemTray.showMessage(strings._('systray_download_completed_title', True), strings._('systray_download_completed_message', True))
# Update the total 'completed downloads' info
self.downloads_completed += 1
self.update_downloads_completed(self.downloads_completed)
# Update the 'in progress downloads' info
self.downloads_in_progress -= 1
self.update_downloads_in_progress(self.downloads_in_progress)
# close on finish?
if not web.get_stay_open():
self.server_status.stop_server()
self.status_bar.showMessage(strings._('closing_automatically', True))
self.status_bar.clearMessage()
self.server_share_status_label.setText(strings._('closing_automatically', True))
else:
if self.server_status.status == self.server_status.STATUS_STOPPED:
self.downloads.cancel_download(event["data"]["id"])
self.downloads_in_progress = 0
self.update_downloads_in_progress(self.downloads_in_progress)
elif event["type"] == web.REQUEST_CANCELED:
self.downloads.cancel_download(event["data"]["id"])
# Update the 'in progress downloads' info
self.downloads_in_progress -= 1
self.update_downloads_in_progress(self.downloads_in_progress)
if self.systemTray.supportsMessages() and self.settings.get('systray_notifications'):
self.systemTray.showMessage(strings._('systray_download_canceled_title', True), strings._('systray_download_canceled_message', True))
@ -487,30 +631,37 @@ class OnionShareGui(QtWidgets.QMainWindow):
# If the auto-shutdown timer has stopped, stop the server
if self.server_status.status == self.server_status.STATUS_STARTED:
if self.app.shutdown_timer and self.server_status.timer_enabled:
if self.app.shutdown_timer and self.settings.get('shutdown_timeout'):
if self.timeout > 0:
now = QtCore.QDateTime.currentDateTime()
seconds_remaining = now.secsTo(self.server_status.timeout)
self.server_status.server_button.setText(strings._('gui_stop_server_shutdown_timeout', True).format(seconds_remaining))
if not self.app.shutdown_timer.is_alive():
# If there were no attempts to download the share, or all downloads are done, we can stop
if web.download_count == 0 or web.done:
self.server_status.stop_server()
self.status_bar.showMessage(strings._('close_on_timeout', True))
self.status_bar.clearMessage()
self.server_share_status_label.setText(strings._('close_on_timeout', True))
# A download is probably still running - hold off on stopping the share
else:
self.status_bar.showMessage(strings._('timeout_download_still_running', True))
self.status_bar.clearMessage()
self.server_share_status_label.setText(strings._('timeout_download_still_running', True))
def copy_url(self):
"""
When the URL gets copied to the clipboard, display this in the status bar.
"""
common.log('OnionShareGui', 'copy_url')
self.status_bar.showMessage(strings._('gui_copied_url', True), 2000)
if self.systemTray.supportsMessages() and self.settings.get('systray_notifications'):
self.systemTray.showMessage(strings._('gui_copied_url_title', True), strings._('gui_copied_url', True))
def copy_hidservauth(self):
"""
When the stealth onion service HidServAuth gets copied to the clipboard, display this in the status bar.
"""
common.log('OnionShareGui', 'copy_hidservauth')
self.status_bar.showMessage(strings._('gui_copied_hidservauth', True), 2000)
if self.systemTray.supportsMessages() and self.settings.get('systray_notifications'):
self.systemTray.showMessage(strings._('gui_copied_hidservauth_title', True), strings._('gui_copied_hidservauth', True))
def clear_message(self):
"""
@ -522,22 +673,52 @@ class OnionShareGui(QtWidgets.QMainWindow):
"""
Disable the Settings button while an OnionShare server is active.
"""
self.settings_button.setEnabled(not active)
if active:
self.settings_button.setIcon( QtGui.QIcon(common.get_resource_path('images/settings_inactive.png')) )
self.settings_button.hide()
else:
self.settings_button.setIcon( QtGui.QIcon(common.get_resource_path('images/settings.png')) )
self.settings_button.show()
# Disable settings menu action when server is active
self.settingsAction.setEnabled(not active)
def reset_info_counters(self):
"""
Set the info counters back to zero.
"""
self.update_downloads_completed(0)
self.update_downloads_in_progress(0)
def update_downloads_completed(self, count):
"""
Update the 'Downloads completed' info widget.
"""
if count == 0:
self.info_completed_downloads_image = common.get_resource_path('images/download_completed_none.png')
else:
self.info_completed_downloads_image = common.get_resource_path('images/download_completed.png')
self.info_completed_downloads_count.setText('<img src="{0:s}" /> {1:d}'.format(self.info_completed_downloads_image, count))
self.info_completed_downloads_count.setToolTip(strings._('info_completed_downloads_tooltip', True).format(count))
def update_downloads_in_progress(self, count):
"""
Update the 'Downloads in progress' info widget.
"""
if count == 0:
self.info_in_progress_downloads_image = common.get_resource_path('images/download_in_progress_none.png')
else:
self.info_in_progress_downloads_image = common.get_resource_path('images/download_in_progress.png')
self.info_in_progress_downloads_count.setText('<img src="{0:s}" /> {1:d}'.format(self.info_in_progress_downloads_image, count))
self.info_in_progress_downloads_count.setToolTip(strings._('info_in_progress_downloads_tooltip', True).format(count))
def closeEvent(self, e):
common.log('OnionShareGui', 'closeEvent')
try:
if self.server_status.status != self.server_status.STATUS_STOPPED:
common.log('OnionShareGui', 'closeEvent, opening warning dialog')
dialog = QtWidgets.QMessageBox()
dialog.setWindowTitle("OnionShare")
dialog.setWindowTitle(strings._('gui_quit_title', True))
dialog.setText(strings._('gui_quit_warning', True))
dialog.setIcon(QtWidgets.QMessageBox.Critical)
quit_button = dialog.addButton(strings._('gui_quit_warning_quit', True), QtWidgets.QMessageBox.YesRole)
dont_quit_button = dialog.addButton(strings._('gui_quit_warning_dont_quit', True), QtWidgets.QMessageBox.NoRole)
dialog.setDefaultButton(dont_quit_button)
@ -566,14 +747,15 @@ class ZipProgressBar(QtWidgets.QProgressBar):
self.setFormat(strings._('zip_progress_bar_format'))
cssStyleData ="""
QProgressBar {
background-color: rgba(255, 255, 255, 0.0) !important;
border: 0px;
border: 1px solid #4e064f;
background-color: #ffffff !important;
text-align: center;
color: #9b9b9b;
}
QProgressBar::chunk {
border: 0px;
background: qlineargradient(x1: 0.5, y1: 0, x2: 0.5, y2: 1, stop: 0 #b366ff, stop: 1 #d9b3ff);
background-color: #4e064f;
width: 10px;
}"""
self.setStyleSheet(cssStyleData)
@ -607,3 +789,26 @@ class ZipProgressBar(QtWidgets.QProgressBar):
self.setValue(100)
else:
self.setValue(0)
class OnionThread(QtCore.QThread):
"""
A QThread for starting our Onion Service.
By using QThread rather than threading.Thread, we are able
to call quit() or terminate() on the startup if the user
decided to cancel (in which case do not proceed with obtaining
the Onion address and starting the web server).
"""
def __init__(self, function, kwargs=None):
super(OnionThread, self).__init__()
common.log('OnionThread', '__init__')
self.function = function
if not kwargs:
self.kwargs = {}
else:
self.kwargs = kwargs
def run(self):
common.log('OnionThread', 'run')
self.function(**self.kwargs)

View File

@ -23,12 +23,14 @@ from PyQt5 import QtCore, QtWidgets, QtGui
from onionshare import strings, common, settings
class ServerStatus(QtWidgets.QVBoxLayout):
class ServerStatus(QtWidgets.QWidget):
"""
The server status chunk of the GUI.
"""
server_started = QtCore.pyqtSignal()
server_stopped = QtCore.pyqtSignal()
server_canceled = QtCore.pyqtSignal()
button_clicked = QtCore.pyqtSignal()
url_copied = QtCore.pyqtSignal()
hidservauth_copied = QtCore.pyqtSignal()
@ -47,100 +49,103 @@ class ServerStatus(QtWidgets.QVBoxLayout):
self.settings = settings
# Helper boolean as this is used in a few places
self.timer_enabled = False
# Shutdown timeout layout
self.server_shutdown_timeout_checkbox = QtWidgets.QCheckBox()
self.server_shutdown_timeout_checkbox.setCheckState(QtCore.Qt.Unchecked)
self.server_shutdown_timeout_checkbox.toggled.connect(self.shutdown_timeout_toggled)
self.server_shutdown_timeout_checkbox.setText(strings._("gui_settings_shutdown_timeout_choice", True))
self.server_shutdown_timeout_label = QtWidgets.QLabel(strings._('gui_settings_shutdown_timeout', True))
self.server_shutdown_timeout = QtWidgets.QDateTimeEdit()
self.shutdown_timeout_label = QtWidgets.QLabel(strings._('gui_settings_shutdown_timeout', True))
self.shutdown_timeout = QtWidgets.QDateTimeEdit()
# Set proposed timeout to be 5 minutes into the future
self.server_shutdown_timeout.setDateTime(QtCore.QDateTime.currentDateTime().addSecs(300))
self.shutdown_timeout.setDisplayFormat("hh:mm A MMM d, yy")
self.shutdown_timeout.setDateTime(QtCore.QDateTime.currentDateTime().addSecs(300))
# Onion services can take a little while to start, so reduce the risk of it expiring too soon by setting the minimum to 2 min from now
self.server_shutdown_timeout.setMinimumDateTime(QtCore.QDateTime.currentDateTime().addSecs(120))
self.server_shutdown_timeout.setCurrentSectionIndex(4)
self.server_shutdown_timeout_label.hide()
self.server_shutdown_timeout.hide()
shutdown_timeout_layout_group = QtWidgets.QHBoxLayout()
shutdown_timeout_layout_group.addWidget(self.server_shutdown_timeout_checkbox)
shutdown_timeout_layout_group.addWidget(self.server_shutdown_timeout_label)
shutdown_timeout_layout_group.addWidget(self.server_shutdown_timeout)
# server layout
self.status_image_stopped = QtGui.QImage(common.get_resource_path('images/server_stopped.png'))
self.status_image_working = QtGui.QImage(common.get_resource_path('images/server_working.png'))
self.status_image_started = QtGui.QImage(common.get_resource_path('images/server_started.png'))
self.status_image_label = QtWidgets.QLabel()
self.status_image_label.setFixedWidth(30)
self.shutdown_timeout.setMinimumDateTime(QtCore.QDateTime.currentDateTime().addSecs(120))
self.shutdown_timeout.setCurrentSection(QtWidgets.QDateTimeEdit.MinuteSection)
shutdown_timeout_layout = QtWidgets.QHBoxLayout()
shutdown_timeout_layout.addWidget(self.shutdown_timeout_label)
shutdown_timeout_layout.addWidget(self.shutdown_timeout)
# Shutdown timeout container, so it can all be hidden and shown as a group
shutdown_timeout_container_layout = QtWidgets.QVBoxLayout()
shutdown_timeout_container_layout.addLayout(shutdown_timeout_layout)
self.shutdown_timeout_container = QtWidgets.QWidget()
self.shutdown_timeout_container.setLayout(shutdown_timeout_container_layout)
self.shutdown_timeout_container.hide()
# Server layout
self.server_button = QtWidgets.QPushButton()
self.server_button.clicked.connect(self.server_button_clicked)
server_layout = QtWidgets.QHBoxLayout()
server_layout.addWidget(self.status_image_label)
server_layout.addWidget(self.server_button)
# url layout
# URL layout
url_font = QtGui.QFont()
self.url_label = QtWidgets.QLabel()
self.url_label.setFont(url_font)
self.url_label.setWordWrap(False)
self.url_label.setAlignment(QtCore.Qt.AlignCenter)
self.url_description = QtWidgets.QLabel(strings._('gui_url_description', True))
self.url_description.setWordWrap(True)
self.url_description.setMinimumHeight(50)
self.url = QtWidgets.QLabel()
self.url.setFont(url_font)
self.url.setWordWrap(True)
self.url.setMinimumHeight(60)
self.url.setMinimumSize(self.url.sizeHint())
self.url.setStyleSheet('QLabel { background-color: #ffffff; color: #000000; padding: 10px; border: 1px solid #666666; }')
url_buttons_style = 'QPushButton { color: #3f7fcf; }'
self.copy_url_button = QtWidgets.QPushButton(strings._('gui_copy_url', True))
self.copy_url_button.setFlat(True)
self.copy_url_button.setStyleSheet(url_buttons_style)
self.copy_url_button.clicked.connect(self.copy_url)
self.copy_hidservauth_button = QtWidgets.QPushButton(strings._('gui_copy_hidservauth', True))
self.copy_hidservauth_button.setFlat(True)
self.copy_hidservauth_button.setStyleSheet(url_buttons_style)
self.copy_hidservauth_button.clicked.connect(self.copy_hidservauth)
url_layout = QtWidgets.QHBoxLayout()
url_layout.addWidget(self.url_label)
url_layout.addWidget(self.copy_url_button)
url_layout.addWidget(self.copy_hidservauth_button)
url_buttons_layout = QtWidgets.QHBoxLayout()
url_buttons_layout.addWidget(self.copy_url_button)
url_buttons_layout.addWidget(self.copy_hidservauth_button)
url_buttons_layout.addStretch()
# add the widgets
self.addLayout(shutdown_timeout_layout_group)
self.addLayout(server_layout)
self.addLayout(url_layout)
url_layout = QtWidgets.QVBoxLayout()
url_layout.addWidget(self.url_description)
url_layout.addWidget(self.url)
url_layout.addLayout(url_buttons_layout)
# Add the widgets
layout = QtWidgets.QVBoxLayout()
layout.addWidget(self.server_button)
layout.addLayout(url_layout)
layout.addWidget(self.shutdown_timeout_container)
self.setLayout(layout)
self.update()
def shutdown_timeout_toggled(self, checked):
"""
Shutdown timer option was toggled. If checked, show the timer settings.
"""
if checked:
self.timer_enabled = True
# Hide the checkbox, show the options
self.server_shutdown_timeout_label.show()
# Reset the default timer to 5 minutes into the future after toggling the option on
self.server_shutdown_timeout.setDateTime(QtCore.QDateTime.currentDateTime().addSecs(300))
self.server_shutdown_timeout.show()
else:
self.timer_enabled = False
self.server_shutdown_timeout_label.hide()
self.server_shutdown_timeout.hide()
def shutdown_timeout_reset(self):
"""
Reset the timeout in the UI after stopping a share
"""
self.server_shutdown_timeout_checkbox.setCheckState(QtCore.Qt.Unchecked)
self.server_shutdown_timeout.setDateTime(QtCore.QDateTime.currentDateTime().addSecs(300))
self.server_shutdown_timeout.setMinimumDateTime(QtCore.QDateTime.currentDateTime().addSecs(120))
self.shutdown_timeout.setDateTime(QtCore.QDateTime.currentDateTime().addSecs(300))
self.shutdown_timeout.setMinimumDateTime(QtCore.QDateTime.currentDateTime().addSecs(120))
def update(self):
"""
Update the GUI elements based on the current state.
"""
# set the status image
if self.status == self.STATUS_STOPPED:
self.status_image_label.setPixmap(QtGui.QPixmap.fromImage(self.status_image_stopped))
elif self.status == self.STATUS_WORKING:
self.status_image_label.setPixmap(QtGui.QPixmap.fromImage(self.status_image_working))
elif self.status == self.STATUS_STARTED:
self.status_image_label.setPixmap(QtGui.QPixmap.fromImage(self.status_image_started))
# set the URL fields
# Set the URL fields
if self.status == self.STATUS_STARTED:
self.url_label.setText('http://{0:s}/{1:s}'.format(self.app.onion_host, self.web.slug))
self.url_label.show()
self.url_description.show()
info_image = common.get_resource_path('images/info.png')
self.url_description.setText(strings._('gui_url_description', True).format(info_image))
# Show a Tool Tip explaining the lifecycle of this URL
if self.settings.get('save_private_key'):
if self.settings.get('close_after_first_download'):
self.url_description.setToolTip(strings._('gui_url_label_onetime_and_persistent', True))
else:
self.url_description.setToolTip(strings._('gui_url_label_persistent', True))
else:
if self.settings.get('close_after_first_download'):
self.url_description.setToolTip(strings._('gui_url_label_onetime', True))
else:
self.url_description.setToolTip(strings._('gui_url_label_stay_open', True))
self.url.setText('http://{0:s}/{1:s}'.format(self.app.onion_host, self.web.slug))
self.url.show()
self.copy_url_button.show()
if self.settings.get('save_private_key'):
@ -148,53 +153,63 @@ class ServerStatus(QtWidgets.QVBoxLayout):
self.settings.set('slug', self.web.slug)
self.settings.save()
if self.settings.get('shutdown_timeout'):
self.shutdown_timeout_container.hide()
if self.app.stealth:
self.copy_hidservauth_button.show()
else:
self.copy_hidservauth_button.hide()
# resize parent widget
p = self.parentWidget()
p.resize(p.sizeHint())
else:
self.url_label.hide()
self.url_description.hide()
self.url.hide()
self.copy_url_button.hide()
self.copy_hidservauth_button.hide()
# button
# Button
button_stopped_style = 'QPushButton { background-color: #5fa416; color: #ffffff; padding: 10px; border: 0; border-radius: 5px; }'
button_working_style = 'QPushButton { background-color: #4c8211; color: #ffffff; padding: 10px; border: 0; border-radius: 5px; font-style: italic; }'
button_started_style = 'QPushButton { background-color: #d0011b; color: #ffffff; padding: 10px; border: 0; border-radius: 5px; }'
if self.file_selection.get_num_files() == 0:
self.server_button.setEnabled(False)
self.server_button.setText(strings._('gui_start_server', True))
self.server_button.hide()
else:
self.server_button.show()
if self.status == self.STATUS_STOPPED:
self.server_button.setStyleSheet(button_stopped_style)
self.server_button.setEnabled(True)
self.server_button.setText(strings._('gui_start_server', True))
self.server_shutdown_timeout.setEnabled(True)
self.server_shutdown_timeout_checkbox.setEnabled(True)
self.server_button.setToolTip('')
if self.settings.get('shutdown_timeout'):
self.shutdown_timeout_container.show()
elif self.status == self.STATUS_STARTED:
self.server_button.setStyleSheet(button_started_style)
self.server_button.setEnabled(True)
self.server_button.setText(strings._('gui_stop_server', True))
self.server_shutdown_timeout.setEnabled(False)
self.server_shutdown_timeout_checkbox.setEnabled(False)
if self.settings.get('shutdown_timeout'):
self.shutdown_timeout_container.hide()
self.server_button.setToolTip(strings._('gui_stop_server_shutdown_timeout_tooltip', True).format(self.timeout))
elif self.status == self.STATUS_WORKING:
self.server_button.setEnabled(False)
self.server_button.setStyleSheet(button_working_style)
self.server_button.setEnabled(True)
self.server_button.setText(strings._('gui_please_wait'))
self.server_shutdown_timeout.setEnabled(False)
self.server_shutdown_timeout_checkbox.setEnabled(False)
if self.settings.get('shutdown_timeout'):
self.shutdown_timeout_container.hide()
else:
self.server_button.setStyleSheet(button_working_style)
self.server_button.setEnabled(False)
self.server_button.setText(strings._('gui_please_wait'))
self.server_shutdown_timeout.setEnabled(False)
self.server_shutdown_timeout_checkbox.setEnabled(False)
if self.settings.get('shutdown_timeout'):
self.shutdown_timeout_container.hide()
def server_button_clicked(self):
"""
Toggle starting or stopping the server.
"""
if self.status == self.STATUS_STOPPED:
if self.timer_enabled:
if self.settings.get('shutdown_timeout'):
# Get the timeout chosen, stripped of its seconds. This prevents confusion if the share stops at (say) 37 seconds past the minute chosen
self.timeout = self.server_shutdown_timeout.dateTime().toPyDateTime().replace(second=0, microsecond=0)
self.timeout = self.shutdown_timeout.dateTime().toPyDateTime().replace(second=0, microsecond=0)
# If the timeout has actually passed already before the user hit Start, refuse to start the server.
if QtCore.QDateTime.currentDateTime().toPyDateTime() > self.timeout:
Alert(strings._('gui_server_timeout_expired', QtWidgets.QMessageBox.Warning))
@ -204,6 +219,9 @@ class ServerStatus(QtWidgets.QVBoxLayout):
self.start_server()
elif self.status == self.STATUS_STARTED:
self.stop_server()
elif self.status == self.STATUS_WORKING:
self.cancel_server()
self.button_clicked.emit()
def start_server(self):
"""
@ -230,6 +248,16 @@ class ServerStatus(QtWidgets.QVBoxLayout):
self.update()
self.server_stopped.emit()
def cancel_server(self):
"""
Cancel the server.
"""
common.log('ServerStatus', 'cancel_server', 'Canceling the server mid-startup')
self.status = self.STATUS_WORKING
self.shutdown_timeout_reset()
self.update()
self.server_canceled.emit()
def stop_server_finished(self):
"""
The server has finished stopping.

View File

@ -60,6 +60,11 @@ class SettingsDialog(QtWidgets.QDialog):
self.systray_notifications_checkbox.setCheckState(QtCore.Qt.Checked)
self.systray_notifications_checkbox.setText(strings._("gui_settings_systray_notifications", True))
# Whether or not to use a shutdown timer
self.shutdown_timeout_checkbox = QtWidgets.QCheckBox()
self.shutdown_timeout_checkbox.setCheckState(QtCore.Qt.Checked)
self.shutdown_timeout_checkbox.setText(strings._("gui_settings_shutdown_timeout_checkbox", True))
# Whether or not to save the Onion private key for reuse
self.save_private_key_checkbox = QtWidgets.QCheckBox()
self.save_private_key_checkbox.setCheckState(QtCore.Qt.Unchecked)
@ -69,6 +74,7 @@ class SettingsDialog(QtWidgets.QDialog):
sharing_group_layout = QtWidgets.QVBoxLayout()
sharing_group_layout.addWidget(self.close_after_first_download_checkbox)
sharing_group_layout.addWidget(self.systray_notifications_checkbox)
sharing_group_layout.addWidget(self.shutdown_timeout_checkbox)
sharing_group_layout.addWidget(self.save_private_key_checkbox)
sharing_group = QtWidgets.QGroupBox(strings._("gui_settings_sharing_label", True))
sharing_group.setLayout(sharing_group_layout)
@ -80,12 +86,14 @@ class SettingsDialog(QtWidgets.QDialog):
stealth_details.setWordWrap(True)
stealth_details.setTextInteractionFlags(QtCore.Qt.TextBrowserInteraction)
stealth_details.setOpenExternalLinks(True)
stealth_details.setMinimumSize(stealth_details.sizeHint())
self.stealth_checkbox = QtWidgets.QCheckBox()
self.stealth_checkbox.setCheckState(QtCore.Qt.Unchecked)
self.stealth_checkbox.setText(strings._("gui_settings_stealth_option", True))
hidservauth_details = QtWidgets.QLabel(strings._('gui_settings_stealth_hidservauth_string', True))
hidservauth_details.setWordWrap(True)
hidservauth_details.setMinimumSize(hidservauth_details.sizeHint())
hidservauth_details.hide()
self.hidservauth_copy_button = QtWidgets.QPushButton(strings._('gui_copy_hidservauth', True))
@ -156,6 +164,26 @@ class SettingsDialog(QtWidgets.QDialog):
self.tor_bridges_use_obfs4_radio = QtWidgets.QRadioButton(strings._('gui_settings_tor_bridges_obfs4_radio_option', True))
self.tor_bridges_use_obfs4_radio.toggled.connect(self.tor_bridges_use_obfs4_radio_toggled)
# meek_lite-amazon option radio
# if the obfs4proxy binary is missing, we can't use meek_lite-amazon transports
(self.tor_path, self.tor_geo_ip_file_path, self.tor_geo_ipv6_file_path, self.obfs4proxy_file_path) = common.get_tor_paths()
if not os.path.isfile(self.obfs4proxy_file_path):
self.tor_bridges_use_meek_lite_amazon_radio = QtWidgets.QRadioButton(strings._('gui_settings_tor_bridges_meek_lite_amazon_radio_option_no_obfs4proxy', True))
self.tor_bridges_use_meek_lite_amazon_radio.setEnabled(False)
else:
self.tor_bridges_use_meek_lite_amazon_radio = QtWidgets.QRadioButton(strings._('gui_settings_tor_bridges_meek_lite_amazon_radio_option', True))
self.tor_bridges_use_meek_lite_amazon_radio.toggled.connect(self.tor_bridges_use_meek_lite_amazon_radio_toggled)
# meek_lite-azure option radio
# if the obfs4proxy binary is missing, we can't use meek_lite-azure transports
(self.tor_path, self.tor_geo_ip_file_path, self.tor_geo_ipv6_file_path, self.obfs4proxy_file_path) = common.get_tor_paths()
if not os.path.isfile(self.obfs4proxy_file_path):
self.tor_bridges_use_meek_lite_azure_radio = QtWidgets.QRadioButton(strings._('gui_settings_tor_bridges_meek_lite_azure_radio_option_no_obfs4proxy', True))
self.tor_bridges_use_meek_lite_azure_radio.setEnabled(False)
else:
self.tor_bridges_use_meek_lite_azure_radio = QtWidgets.QRadioButton(strings._('gui_settings_tor_bridges_meek_lite_azure_radio_option', True))
self.tor_bridges_use_meek_lite_azure_radio.toggled.connect(self.tor_bridges_use_meek_lite_azure_radio_toggled)
# Custom bridges radio and textbox
self.tor_bridges_use_custom_radio = QtWidgets.QRadioButton(strings._('gui_settings_tor_bridges_custom_radio_option', True))
self.tor_bridges_use_custom_radio.toggled.connect(self.tor_bridges_use_custom_radio_toggled)
@ -179,6 +207,8 @@ class SettingsDialog(QtWidgets.QDialog):
bridges_layout = QtWidgets.QVBoxLayout()
bridges_layout.addWidget(self.tor_bridges_no_bridges_radio)
bridges_layout.addWidget(self.tor_bridges_use_obfs4_radio)
bridges_layout.addWidget(self.tor_bridges_use_meek_lite_amazon_radio)
bridges_layout.addWidget(self.tor_bridges_use_meek_lite_azure_radio)
bridges_layout.addWidget(self.tor_bridges_use_custom_radio)
bridges_layout.addWidget(self.tor_bridges_use_custom_textbox_options)
@ -295,9 +325,12 @@ class SettingsDialog(QtWidgets.QDialog):
self.save_button.clicked.connect(self.save_clicked)
self.cancel_button = QtWidgets.QPushButton(strings._('gui_settings_button_cancel', True))
self.cancel_button.clicked.connect(self.cancel_clicked)
version_label = QtWidgets.QLabel('OnionShare {0:s}'.format(common.get_version()))
version_label.setStyleSheet('color: #666666')
self.help_button = QtWidgets.QPushButton(strings._('gui_settings_button_help', True))
self.help_button.clicked.connect(self.help_clicked)
buttons_layout = QtWidgets.QHBoxLayout()
buttons_layout.addWidget(version_label)
buttons_layout.addWidget(self.help_button)
buttons_layout.addStretch()
buttons_layout.addWidget(self.save_button)
@ -349,6 +382,12 @@ class SettingsDialog(QtWidgets.QDialog):
else:
self.systray_notifications_checkbox.setCheckState(QtCore.Qt.Unchecked)
shutdown_timeout = self.old_settings.get('shutdown_timeout')
if shutdown_timeout:
self.shutdown_timeout_checkbox.setCheckState(QtCore.Qt.Checked)
else:
self.shutdown_timeout_checkbox.setCheckState(QtCore.Qt.Unchecked)
save_private_key = self.old_settings.get('save_private_key')
if save_private_key:
self.save_private_key_checkbox.setCheckState(QtCore.Qt.Checked)
@ -401,10 +440,15 @@ class SettingsDialog(QtWidgets.QDialog):
if self.old_settings.get('no_bridges'):
self.tor_bridges_no_bridges_radio.setChecked(True)
self.tor_bridges_use_obfs4_radio.setChecked(False)
self.tor_bridges_use_meek_lite_amazon_radio.setChecked(False)
self.tor_bridges_use_meek_lite_azure_radio.setChecked(False)
self.tor_bridges_use_custom_radio.setChecked(False)
else:
self.tor_bridges_no_bridges_radio.setChecked(False)
self.tor_bridges_use_obfs4_radio.setChecked(self.old_settings.get('tor_bridges_use_obfs4'))
self.tor_bridges_use_meek_lite_amazon_radio.setChecked(self.old_settings.get('tor_bridges_use_meek_lite_amazon'))
self.tor_bridges_use_meek_lite_azure_radio.setChecked(self.old_settings.get('tor_bridges_use_meek_lite_azure'))
if self.old_settings.get('tor_bridges_use_custom_bridges'):
self.tor_bridges_use_custom_radio.setChecked(True)
# Remove the 'Bridge' lines at the start of each bridge.
@ -441,6 +485,20 @@ class SettingsDialog(QtWidgets.QDialog):
if checked:
self.tor_bridges_use_custom_textbox_options.hide()
def tor_bridges_use_meek_lite_amazon_radio_toggled(self, checked):
"""
meek_lite-amazon bridges option was toggled. If checked, disable custom bridge options.
"""
if checked:
self.tor_bridges_use_custom_textbox_options.hide()
def tor_bridges_use_meek_lite_azure_radio_toggled(self, checked):
"""
meek_lite_azure bridges option was toggled. If checked, disable custom bridge options.
"""
if checked:
self.tor_bridges_use_custom_textbox_options.hide()
def tor_bridges_use_custom_radio_toggled(self, checked):
"""
Custom bridges option was toggled. If checked, show custom bridge options.
@ -557,31 +615,43 @@ class SettingsDialog(QtWidgets.QDialog):
self._disable_buttons()
self.qtapp.processEvents()
def update_timestamp():
# Update the last checked label
settings = Settings(self.config)
settings.load()
autoupdate_timestamp = settings.get('autoupdate_timestamp')
self._update_autoupdate_timestamp(autoupdate_timestamp)
def close_forced_update_thread():
forced_update_thread.quit()
# Enable buttons
self._enable_buttons()
# Update timestamp
update_timestamp()
# Check for updates
def update_available(update_url, installed_version, latest_version):
Alert(strings._("update_available", True).format(update_url, installed_version, latest_version))
close_forced_update_thread()
def update_not_available():
Alert(strings._('update_not_available', True))
close_forced_update_thread()
u = UpdateChecker(self.onion)
u.update_available.connect(update_available)
u.update_not_available.connect(update_not_available)
try:
u.check(force=True)
except UpdateCheckerCheckError:
def update_error():
Alert(strings._('update_error_check_error', True), QtWidgets.QMessageBox.Warning)
except UpdateCheckerInvalidLatestVersion as e:
close_forced_update_thread()
def update_invalid_version():
Alert(strings._('update_error_invalid_latest_version', True).format(e.latest_version), QtWidgets.QMessageBox.Warning)
close_forced_update_thread()
# Enable buttons
self._enable_buttons()
# Update the last checked label
settings = Settings(self.config)
settings.load()
autoupdate_timestamp = settings.get('autoupdate_timestamp')
self._update_autoupdate_timestamp(autoupdate_timestamp)
forced_update_thread = UpdateThread(self.onion, self.config, force=True)
forced_update_thread.update_available.connect(update_available)
forced_update_thread.update_not_available.connect(update_not_available)
forced_update_thread.update_error.connect(update_error)
forced_update_thread.update_invalid_version.connect(update_invalid_version)
forced_update_thread.start()
def save_clicked(self):
"""
@ -590,56 +660,58 @@ class SettingsDialog(QtWidgets.QDialog):
common.log('SettingsDialog', 'save_clicked')
settings = self.settings_from_fields()
settings.save()
if settings:
settings.save()
# If Tor isn't connected, or if Tor settings have changed, Reinitialize
# the Onion object
reboot_onion = False
if self.onion.is_authenticated():
common.log('SettingsDialog', 'save_clicked', 'Connected to Tor')
def changed(s1, s2, keys):
"""
Compare the Settings objects s1 and s2 and return true if any values
have changed for the given keys.
"""
for key in keys:
if s1.get(key) != s2.get(key):
return True
return False
# If Tor isn't connected, or if Tor settings have changed, Reinitialize
# the Onion object
reboot_onion = False
if self.onion.is_authenticated():
common.log('SettingsDialog', 'save_clicked', 'Connected to Tor')
def changed(s1, s2, keys):
"""
Compare the Settings objects s1 and s2 and return true if any values
have changed for the given keys.
"""
for key in keys:
if s1.get(key) != s2.get(key):
return True
return False
if changed(settings, self.old_settings, [
'connection_type', 'control_port_address',
'control_port_port', 'socks_address', 'socks_port',
'socket_file_path', 'auth_type', 'auth_password',
'no_bridges', 'tor_bridges_use_obfs4',
'tor_bridges_use_custom_bridges']):
if changed(settings, self.old_settings, [
'connection_type', 'control_port_address',
'control_port_port', 'socks_address', 'socks_port',
'socket_file_path', 'auth_type', 'auth_password',
'no_bridges', 'tor_bridges_use_obfs4',
'tor_bridges_use_meek_lite_amazon', 'tor_bridges_use_meek_lite_azure',
'tor_bridges_use_custom_bridges']):
reboot_onion = True
else:
common.log('SettingsDialog', 'save_clicked', 'Not connected to Tor')
# Tor isn't connected, so try connecting
reboot_onion = True
else:
common.log('SettingsDialog', 'save_clicked', 'Not connected to Tor')
# Tor isn't connected, so try connecting
reboot_onion = True
# Do we need to reinitialize Tor?
if reboot_onion:
# Reinitialize the Onion object
common.log('SettingsDialog', 'save_clicked', 'rebooting the Onion')
self.onion.cleanup()
# Do we need to reinitialize Tor?
if reboot_onion:
# Reinitialize the Onion object
common.log('SettingsDialog', 'save_clicked', 'rebooting the Onion')
self.onion.cleanup()
tor_con = TorConnectionDialog(self.qtapp, settings, self.onion)
tor_con.start()
tor_con = TorConnectionDialog(self.qtapp, settings, self.onion)
tor_con.start()
common.log('SettingsDialog', 'save_clicked', 'Onion done rebooting, connected to Tor: {}'.format(self.onion.connected_to_tor))
common.log('SettingsDialog', 'save_clicked', 'Onion done rebooting, connected to Tor: {}'.format(self.onion.connected_to_tor))
if self.onion.is_authenticated() and not tor_con.wasCanceled():
self.settings_saved.emit()
self.close()
if self.onion.is_authenticated() and not tor_con.wasCanceled():
else:
self.settings_saved.emit()
self.close()
else:
self.settings_saved.emit()
self.close()
def cancel_clicked(self):
"""
Cancel button clicked.
@ -669,6 +741,7 @@ class SettingsDialog(QtWidgets.QDialog):
settings.set('close_after_first_download', self.close_after_first_download_checkbox.isChecked())
settings.set('systray_notifications', self.systray_notifications_checkbox.isChecked())
settings.set('shutdown_timeout', self.shutdown_timeout_checkbox.isChecked())
if self.save_private_key_checkbox.isChecked():
settings.set('save_private_key', True)
settings.set('private_key', self.old_settings.get('private_key'))
@ -717,14 +790,33 @@ class SettingsDialog(QtWidgets.QDialog):
if self.tor_bridges_no_bridges_radio.isChecked():
settings.set('no_bridges', True)
settings.set('tor_bridges_use_obfs4', False)
settings.set('tor_bridges_use_meek_lite_amazon', False)
settings.set('tor_bridges_use_meek_lite_azure', False)
settings.set('tor_bridges_use_custom_bridges', '')
if self.tor_bridges_use_obfs4_radio.isChecked():
settings.set('no_bridges', False)
settings.set('tor_bridges_use_obfs4', True)
settings.set('tor_bridges_use_meek_lite_amazon', False)
settings.set('tor_bridges_use_meek_lite_azure', False)
settings.set('tor_bridges_use_custom_bridges', '')
if self.tor_bridges_use_meek_lite_amazon_radio.isChecked():
settings.set('no_bridges', False)
settings.set('tor_bridges_use_obfs4', False)
settings.set('tor_bridges_use_meek_lite_amazon', True)
settings.set('tor_bridges_use_meek_lite_azure', False)
settings.set('tor_bridges_use_custom_bridges', '')
if self.tor_bridges_use_meek_lite_azure_radio.isChecked():
settings.set('no_bridges', False)
settings.set('tor_bridges_use_obfs4', False)
settings.set('tor_bridges_use_meek_lite_amazon', False)
settings.set('tor_bridges_use_meek_lite_azure', True)
settings.set('tor_bridges_use_custom_bridges', '')
if self.tor_bridges_use_custom_radio.isChecked():
settings.set('no_bridges', False)
settings.set('tor_bridges_use_obfs4', False)
settings.set('tor_bridges_use_meek_lite_amazon', False)
settings.set('tor_bridges_use_meek_lite_azure', False)
# Insert a 'Bridge' line at the start of each bridge.
# This makes it easier to copy/paste a set of bridges
# provided from https://bridges.torproject.org
@ -734,8 +826,12 @@ class SettingsDialog(QtWidgets.QDialog):
for bridge in bridges:
if bridge != '':
# Check the syntax of the custom bridge to make sure it looks legitimate
pattern = re.compile("[0-9.]+:[0-9]+\s[A-Z0-9]+$")
if pattern.match(bridge):
ipv4_pattern = re.compile("(obfs4\s+)?(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]):([0-9]+)(\s+)([A-Z0-9]+)(.+)$")
ipv6_pattern = re.compile("(obfs4\s+)?\[(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))\]:[0-9]+\s+[A-Z0-9]+(.+)$")
meek_lite_pattern = re.compile("(meek_lite)(\s)+([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+:[0-9]+)(\s)+([0-9A-Z]+)(\s)+url=(.+)(\s)+front=(.+)")
if ipv4_pattern.match(bridge) or \
ipv6_pattern.match(bridge) or \
meek_lite_pattern.match(bridge):
new_bridges.append(''.join(['Bridge ', bridge, '\n']))
bridges_valid = True
if bridges_valid:
@ -744,6 +840,7 @@ class SettingsDialog(QtWidgets.QDialog):
else:
Alert(strings._('gui_settings_tor_bridges_invalid', True))
settings.set('no_bridges', True)
return False
return settings

View File

@ -19,6 +19,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
from PyQt5 import QtCore
import datetime, time, socket, re, platform
from distutils.version import LooseVersion as Version
from onionshare import socks
from onionshare.settings import Settings
@ -51,6 +52,8 @@ class UpdateChecker(QtCore.QObject):
"""
update_available = QtCore.pyqtSignal(str, str, str)
update_not_available = QtCore.pyqtSignal()
update_error = QtCore.pyqtSignal()
update_invalid_version = QtCore.pyqtSignal()
def __init__(self, onion, config=False):
super(UpdateChecker, self).__init__()
@ -96,7 +99,10 @@ class UpdateChecker(QtCore.QObject):
if force:
path += '?force=1'
onion_domain = 'elx57ue5uyfplgva.onion'
if Version(self.onion.tor_version) >= Version('0.3.2.9'):
onion_domain = 'lldan5gahapx5k7iafb3s4ikijc4ni7gx5iywdflkba5y2ezyg6sjgyd.onion'
else:
onion_domain = 'elx57ue5uyfplgva.onion'
common.log('UpdateChecker', 'check', 'loading http://{}{}'.format(onion_domain, path))
@ -120,12 +126,14 @@ class UpdateChecker(QtCore.QObject):
except Exception as e:
common.log('UpdateChecker', 'check', '{}'.format(e))
self.update_error.emit()
raise UpdateCheckerCheckError
# Validate that latest_version looks like a version string
# This regex is: 1-3 dot-separated numeric components
version_re = r"^(\d+\.)?(\d+\.)?(\d+)$"
if not re.match(version_re, latest_version):
self.update_invalid_version.emit()
raise UpdateCheckerInvalidLatestVersion(latest_version)
# Update the last checked timestamp (dropping the seconds and milliseconds)
@ -148,12 +156,15 @@ class UpdateChecker(QtCore.QObject):
class UpdateThread(QtCore.QThread):
update_available = QtCore.pyqtSignal(str, str, str)
update_not_available = QtCore.pyqtSignal()
update_error = QtCore.pyqtSignal()
update_invalid_version = QtCore.pyqtSignal()
def __init__(self, onion, config=False):
def __init__(self, onion, config=False, force=False):
super(UpdateThread, self).__init__()
common.log('UpdateThread', '__init__')
self.onion = onion
self.config = config
self.force = force
def run(self):
common.log('UpdateThread', 'run')
@ -161,9 +172,11 @@ class UpdateThread(QtCore.QThread):
u = UpdateChecker(self.onion, self.config)
u.update_available.connect(self._update_available)
u.update_not_available.connect(self._update_not_available)
u.update_error.connect(self._update_error)
u.update_invalid_version.connect(self._update_invalid_version)
try:
u.check(config=self.config)
u.check(config=self.config,force=self.force)
except Exception as e:
# If update check fails, silently ignore
common.log('UpdateThread', 'run', '{}'.format(e))
@ -178,3 +191,13 @@ class UpdateThread(QtCore.QThread):
common.log('UpdateThread', '_update_not_available')
self.active = False
self.update_not_available.emit()
def _update_error(self):
common.log('UpdateThread', '_update_error')
self.active = False
self.update_error.emit()
def _update_invalid_version(self):
common.log('UpdateThread', '_update_invalid_version')
self.active = False
self.update_invalid_version.emit()

Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 KiB

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 105 KiB

After

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 KiB

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 45 KiB

View File

@ -45,6 +45,17 @@ author_email = 'micah@micahflee.com'
url = 'https://github.com/micahflee/onionshare'
license = 'GPL v3'
keywords = 'onion, share, onionshare, tor, anonymous, web server'
data_files=[
(os.path.join(sys.prefix, 'share/applications'), ['install/onionshare.desktop']),
(os.path.join(sys.prefix, 'share/appdata'), ['install/onionshare.appdata.xml']),
(os.path.join(sys.prefix, 'share/pixmaps'), ['install/onionshare80.xpm']),
(os.path.join(sys.prefix, 'share/onionshare'), file_list('share')),
(os.path.join(sys.prefix, 'share/onionshare/images'), file_list('share/images')),
(os.path.join(sys.prefix, 'share/onionshare/locale'), file_list('share/locale')),
(os.path.join(sys.prefix, 'share/onionshare/html'), file_list('share/html')),
]
if platform.system() != 'OpenBSD':
data_files.append(('/usr/share/nautilus-python/extensions/', ['install/scripts/onionshare-nautilus.py']))
setup(
name='onionshare', version=version,
@ -54,14 +65,5 @@ setup(
packages=['onionshare', 'onionshare_gui'],
include_package_data=True,
scripts=['install/scripts/onionshare', 'install/scripts/onionshare-gui'],
data_files=[
(os.path.join(sys.prefix, 'share/applications'), ['install/onionshare.desktop']),
(os.path.join(sys.prefix, 'share/appdata'), ['install/onionshare.appdata.xml']),
(os.path.join(sys.prefix, 'share/pixmaps'), ['install/onionshare80.xpm']),
(os.path.join(sys.prefix, 'share/onionshare'), file_list('share')),
(os.path.join(sys.prefix, 'share/onionshare/images'), file_list('share/images')),
(os.path.join(sys.prefix, 'share/onionshare/locale'), file_list('share/locale')),
(os.path.join(sys.prefix, 'share/onionshare/html'), file_list('share/html')),
('/usr/share/nautilus-python/extensions/', ['install/scripts/onionshare-nautilus.py'])
]
data_files=data_files
)

View File

@ -2,6 +2,7 @@
<html>
<head>
<title>Error 404</title>
<link href="data:image/x-icon;base64,{{favicon_b64}}" rel="icon" type="image/x-icon" />
<style type="text/css">
body {
background-color: #FFC4D5;

View File

@ -2,6 +2,7 @@
<html>
<head>
<title>OnionShare</title>
<link href="data:image/x-icon;base64,{{favicon_b64}}" rel="icon" type="image/x-icon" />
<style>
body {
background-color: #222222;
@ -15,4 +16,4 @@
<body>
<p>OnionShare download in progress</p>
</body>
</html>
</html>

View File

@ -2,106 +2,207 @@
<html>
<head>
<title>OnionShare</title>
<link href="data:image/x-icon;base64,{{favicon_b64}}" rel="icon" type="image/x-icon" />
<style type="text/css">
body {
background-color: #222222;
color: #ffffff;
text-align: center;
font-family: sans-serif;
padding: 5em 1em;
}
.button {
-moz-box-shadow:inset 0px 1px 0px 0px #cae3fc;
-webkit-box-shadow:inset 0px 1px 0px 0px #cae3fc;
box-shadow:inset 0px 1px 0px 0px #cae3fc;
background:-webkit-gradient( linear, left top, left bottom, color-stop(0.05, #79bbff), color-stop(1, #4197ee) );
background:-moz-linear-gradient( center top, #79bbff 5%, #4197ee 100% );
filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#79bbff', endColorstr='#4197ee');
background-color:#79bbff;
-webkit-border-top-left-radius:12px;
-moz-border-radius-topleft:12px;
border-top-left-radius:12px;
-webkit-border-top-right-radius:12px;
-moz-border-radius-topright:12px;
border-top-right-radius:12px;
-webkit-border-bottom-right-radius:12px;
-moz-border-radius-bottomright:12px;
border-bottom-right-radius:12px;
-webkit-border-bottom-left-radius:12px;
-moz-border-radius-bottomleft:12px;
border-bottom-left-radius:12px;
text-indent:0;
border:1px solid #469df5;
display:inline-block;
color:#ffffff;
font-size:29px;
font-weight:bold;
font-style:normal;
height:50px;
line-height:50px;
text-decoration:none;
text-align:center;
text-shadow:1px 1px 0px #287ace;
padding: 0 20px;
}
.button:hover {
background:-webkit-gradient( linear, left top, left bottom, color-stop(0.05, #4197ee), color-stop(1, #79bbff) );
background:-moz-linear-gradient( center top, #4197ee 5%, #79bbff 100% );
filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#4197ee', endColorstr='#79bbff');
background-color:#4197ee;
}.button:active {
position:relative;
top:1px;
}
.clearfix:after {
content: ".";
display: block;
clear: both;
visibility: hidden;
line-height: 0;
height: 0;
}
.download-size {
color: #999999;
}
.download-description {
padding: 10px;
}
.file-list {
margin: 50px auto 0 auto;
padding: 10px;
text-align: left;
background-color: #333333;
}
.file-list th {
padding: 5px;
font-weight: bold;
color: #999999;
}
.file-list td {
padding: 5px;
}
body {
margin: 0;
font-family: Helvetica;
}
header {
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
background: #fcfcfc;
background: -webkit-linear-gradient(top, #fcfcfc 0%, #f2f2f2 100%);
padding: 0.8rem;
}
header .logo {
vertical-align: middle;
width: 45px;
height: 45px;
}
header h1 {
display: inline-block;
margin: 0 0 0 0.5rem;
vertical-align: middle;
font-weight: normal;
font-size: 1.5rem;
color: #666666;
}
header .right {
float: right;
font-size: .75rem;
}
header .right ul li {
display: inline;
margin: 0 0 0 .5rem;
font-size: 1rem;
}
header .button {
color: #ffffff;
background-color: #4e064f;
padding: 10px;
border-radius: 5px;
text-decoration: none;
margin-left: 1rem;
cursor: pointer;
}
table.file-list {
width: 100%;
margin: 0 auto;
border-collapse: collapse;
}
table.file-list th {
text-align: left;
text-transform: uppercase;
font-weight: normal;
color: #666666;
padding: 0.5rem;
}
table.file-list tr {
border-bottom: 1px solid #e0e0e0;
}
table.file-list td {
white-space: nowrap;
padding: 0.5rem 10rem 0.5rem 0.8rem;
}
table.file-list td img {
vertical-align: middle;
margin-right: 0.5rem;
}
table.file-list td:last-child {
width: 100%;
}
</style>
<meta name="onionshare-filename" content="{{ filename }}">
<meta name="onionshare-filesize" content="{{ filesize }}">
</head>
<body>
<p><a class="button" href='/{{ slug }}/download'>{{ filename }} &#x25BC;</a></p>
<p class="download-size"><strong title="{{ filesize }} bytes">{{ filesize_human }} (compressed)</strong></p>
<p class="download-description">This zip file contains the following contents:</p>
<table class="file-list">
<header class="clearfix">
<div class="right">
<ul>
<li>Total size: <strong>{{ filesize_human }}</strong> (compressed)</li>
<li><a class="button" href='/{{ slug }}/download'>Download Files</a></li>
</ul>
</div>
<img class="logo" src="data:image/png;base64,{{logo_b64}}" title="OnionShare">
<h1>OnionShare</h1>
</header>
<table class="file-list" id="file-list">
<tr>
<th>Type</th>
<th>Name</th>
<th>Size</th>
<th onclick="sortTable(0)">Filename</th>
<th onclick="sortTable(1)">Size</th>
<th></th>
</tr>
{% for info in file_info.dirs %}
<tr>
<td><img width="30" height="30" title="" alt="" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAAeCAYAAAA7MK6iAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAA7EAAAOxAGVKw4bAAAAB3RJTUUH3ggbFCQXOpwjbQAAAM5JREFUSMftlksOwyAMRDFCbNjkDL1kz9K75Cq9AZF3kYIM7oqoP/KpcFK1mRUrnsbW2Fbq0N+JiPhZRMS1OZAfzLzocwCAGmC9V2Vhjduarj8CzynGqIwxsDl4SXV267E4uFTN33X8deCxDyV1XXeRYJo1UUDEa9M0pxoRK+b4HiqRcz3nVAJKRK+TSxqaGbrkNKUkBn0oNSK2+T0MA1dau9NzO4QwuvPen1lAxQtEsq/vNpSWhvZ9v/3IRMTWOQezkyuEoKy14tfHoU11A6Mr5AxrpuMVAAAAAElFTkSuQmCC" /></td>
<td>{{ info.basename }}</td>
<td>
<img width="30" height="30" title="" alt="" src="data:image/png;base64,{{ folder_b64 }}" />
{{ info.basename }}
</td>
<td>{{ info.size_human }}</td>
<td></td>
</tr>
{% endfor %}
{% for info in file_info.files %}
<tr>
<td><img width="30" height="30" title="" alt="" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAAeCAYAAAA7MK6iAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAA7EAAAOxAGVKw4bAAAAB3RJTUUH3ggbFCQqYvRvfAAAAJFJREFUSMftlUkOwCAIAEWJ//+uIUivtYmtG7VN8KYHRhlA5zYtuB6klCTG2BcEAHrBeN4ws3jvX3lxQZmFiogMgVesVrhKXlvgakKf4KqVdAdXL+Ea3O8aILh0GnUMEhjtw9mLbUv1tx33fgItynBVoN+k2hybY3Nsjg08Bs45q4GYuQ4OIQARLYcSkUPEoiUP4j5IFasKOnUAAAAASUVORK5CYII=" /></td>
<td>{{ info.basename }}</td>
<td>
<img width="30" height="30" title="" alt="" src="data:image/png;base64,{{ file_b64 }}" />
{{ info.basename }}
</td>
<td>{{ info.size_human }}</td>
<td></td>
</tr>
{% endfor %}
</table>
<script>
// Function to convert human-readable sizes back to bytes, for sorting
function unhumanize(text) {
var powers = {'b': 0, 'k': 1, 'm': 2, 'g': 3, 't': 4};
var regex = /(\d+(?:\.\d+)?)\s?(B|K|M|G|T)?/i;
var res = regex.exec(text);
if(res[2] === undefined) {
// Account for alphabetical words (file/dir names)
return text;
} else {
return res[1] * Math.pow(1024, powers[res[2].toLowerCase()]);
}
}
function sortTable(n) {
var table, rows, switching, i, x, y, shouldSwitch, dir, switchcount = 0;
table = document.getElementById("file-list");
switching = true;
// Set the sorting direction to ascending:
dir = "asc";
/* Make a loop that will continue until
no switching has been done: */
while (switching) {
// Start by saying: no switching is done:
switching = false;
rows = table.getElementsByTagName("TR");
/* Loop through all table rows (except the
first, which contains table headers): */
for (i = 1; i < (rows.length - 1); i++) {
// Start by saying there should be no switching:
shouldSwitch = false;
/* Get the two elements you want to compare,
one from current row and one from the next: */
x = rows[i].getElementsByTagName("TD")[n];
y = rows[i + 1].getElementsByTagName("TD")[n];
/* Check if the two rows should switch place,
based on the direction, asc or desc: */
if (dir == "asc") {
if (unhumanize(x.innerHTML.toLowerCase()) > unhumanize(y.innerHTML.toLowerCase())) {
// If so, mark as a switch and break the loop:
shouldSwitch= true;
break;
}
} else if (dir == "desc") {
if (unhumanize(x.innerHTML.toLowerCase()) < unhumanize(y.innerHTML.toLowerCase())) {
// If so, mark as a switch and break the loop:
shouldSwitch= true;
break;
}
}
}
if (shouldSwitch) {
/* If a switch has been marked, make the switch
and mark that a switch has been done: */
rows[i].parentNode.insertBefore(rows[i + 1], rows[i]);
switching = true;
// Each time a switch is done, increase this count by 1:
switchcount ++;
} else {
/* If no switching has been done AND the direction is "asc",
set the direction to "desc" and run the while loop again. */
if (switchcount == 0 && dir == "asc") {
dir = "desc";
switching = true;
}
}
}
}
</script>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 646 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 437 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 638 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 412 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

BIN
share/images/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 182 B

BIN
share/images/info.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 435 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 346 B

After

Width:  |  Height:  |  Size: 347 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 286 B

After

Width:  |  Height:  |  Size: 342 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 338 B

After

Width:  |  Height:  |  Size: 349 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 513 B

BIN
share/images/web_file.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 251 B

BIN
share/images/web_folder.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 338 B

136
share/locale/da.json Normal file
View File

@ -0,0 +1,136 @@
{
"config_onion_service": "Konfigurerer onion-tjeneste på port {0:d}.",
"preparing_files": "Forbereder filer som skal deles.",
"wait_for_hs": "Venter på at HS bliver klar:",
"wait_for_hs_trying": "Prøver...",
"wait_for_hs_nope": "Endnu ikke klar.",
"wait_for_hs_yup": "Klar!",
"give_this_url": "Giv denne URL til personen du sender filen til:",
"give_this_url_stealth": "Giv denne URL og HidServAuth-linje til personen du sender filen til:",
"ctrlc_to_stop": "Tryk på Ctrl-C for at stoppe serveren",
"not_a_file": "{0:s} er ikke en gyldig fil.",
"not_a_readable_file": "{0:s} er ikke en læsbar fil.",
"no_available_port": "Kunne ikke starte onion-tjenesten da der ikke var nogen tilgængelig port.",
"download_page_loaded": "Downloadside indlæst",
"other_page_loaded": "URL indlæst",
"close_on_timeout": "Lukker automatisk da timeout er nået",
"closing_automatically": "Lukker automatisk da download er færdig",
"timeout_download_still_running": "Venter på at download skal blive færdig inden automatisk stop",
"large_filesize": "Advarsel: Det kan tage timer at sende store filer",
"error_tails_invalid_port": "Ugyldig værdi, port skal være et heltal",
"error_tails_unknown_root": "Ukendt fejl med Tails-rodproces",
"systray_menu_exit": "Afslut",
"systray_download_started_title": "OnionShare-download startet",
"systray_download_started_message": "En bruger startede download af dine filer",
"systray_download_completed_title": "OnionShare-download færdig",
"systray_download_completed_message": "Brugeren er færdig med at downloade dine filer",
"systray_download_canceled_title": "OnionShare-download annulleret",
"systray_download_canceled_message": "Brugeren annullerede downloaden",
"help_local_only": "Undlad at bruge tor: kun til udvikling",
"help_stay_open": "Hold onion-tjeneste kørende efter download er færdig",
"help_shutdown_timeout": "Luk onion-tjenesten efter N sekunder",
"help_transparent_torification": "Mit system er gennemsigtigt torifiseret",
"help_stealth": "Opret usynlig onion-tjeneste (avanceret)",
"help_debug": "Log programfejl til stdout, og log webfejl til disk",
"help_filename": "Liste over filer eller mapper som skal deles",
"help_config": "Sti til en brugerdefineret JSON-konfigurationsfil (valgfri)",
"gui_drag_and_drop": "Træk og slip\nfiler her",
"gui_add": "Tilføj",
"gui_delete": "Slet",
"gui_choose_items": "Vælg",
"gui_start_server": "Start deling",
"gui_stop_server": "Stop deling",
"gui_copy_url": "Kopiér URL",
"gui_copy_hidservauth": "Kopiér HidServAuth",
"gui_downloads": "Downloads:",
"gui_canceled": "Annulleret",
"gui_copied_url": "Kopierede URL til udklipsholder",
"gui_copied_hidservauth": "Kopierede HidServAuth-linje til udklipsholder",
"gui_starting_server1": "Starter Tor onion-tjeneste...",
"gui_starting_server2": "Databehandler filer...",
"gui_please_wait": "Vent venligst...",
"error_hs_dir_cannot_create": "Kan ikke oprette onion-tjenestens mappe {0:s}",
"error_hs_dir_not_writable": "onion-tjenestens mappe {0:s} er skrivebeskyttet",
"using_ephemeral": "Starter kortvarig Tor onion-tjeneste og afventer udgivelse",
"gui_download_progress_complete": "%p%, tid forløbet: {0:s}",
"gui_download_progress_starting": "{0:s}, %p% (udregner anslået ankomsttid)",
"gui_download_progress_eta": "{0:s}, anslået ankomsttid: {1:s}, %p%",
"version_string": "Onionshare {0:s} | https://onionshare.org/",
"gui_quit_warning": "Er du sikker på, at du vil afslutte?\nURL'en som du deler vil ikke eksistere længere.",
"gui_quit_warning_quit": "Afslut",
"gui_quit_warning_dont_quit": "Afslut ikke",
"error_rate_limit": "En angriber forsøger måske at gætte din URL. For at forhindre det, har OnionShare automatisk stoppet serveren. For at dele filerne skal du starte den igen og dele den nye URL.",
"zip_progress_bar_format": "Databehandler filer: %p%",
"error_stealth_not_supported": "For at oprette usynlige onion-tjenester, skal du mindst have Tor 0.2.9.1-alpha (eller Tor Browser 6.5) og mindst python3-stem 1.5.0.",
"error_ephemeral_not_supported": "OnionShare kræver mindst Tor 0.2.7.1 og mindst python3-stem 1.4.0.",
"gui_settings_window_title": "Indstillinger",
"gui_settings_stealth_label": "Usynlig (avanceret)",
"gui_settings_stealth_option": "Opret usynlige onion-tjenester",
"gui_settings_stealth_option_details": "Det gør OnionShare mere sikker, men også mere besværlig for modtageren at oprette forbindelse til den.<br><a href=\"https://github.com/micahflee/onionshare/wiki/Stealth-Onion-Services\">Mere information</a>.",
"gui_settings_stealth_hidservauth_string": "Du har gemt den private nøgle til at blive brugt igen, så din HidServAuth-streng bruges også igen.\nKlik nedenfor, for at kopiere HidServAuth.",
"gui_settings_autoupdate_label": "Søg efter opdateringer",
"gui_settings_autoupdate_option": "Giv mig besked når der findes opdateringer",
"gui_settings_autoupdate_timestamp": "Sidste søgning: {}",
"gui_settings_autoupdate_timestamp_never": "Aldrig",
"gui_settings_autoupdate_check_button": "Søg efter opdateringer",
"gui_settings_sharing_label": "Valgmuligheder for deling",
"gui_settings_close_after_first_download_option": "Stop deling efter første download",
"gui_settings_systray_notifications": "Vis skrivebordsnotifikationer",
"gui_settings_connection_type_label": "Hvordan skal OnionShare oprette forbindelse til Tor?",
"gui_settings_connection_type_bundled_option": "Brug Tor som er bundet med OnionShare",
"gui_settings_connection_type_automatic_option": "Prøv automatisk konfiguration med Tor Browser",
"gui_settings_connection_type_control_port_option": "Opret forbindelse med kontrolport",
"gui_settings_connection_type_socket_file_option": "Opret forbindelse med sokkelfil",
"gui_settings_connection_type_test_button": "Test Tor-indstillinger",
"gui_settings_control_port_label": "Kontrolport",
"gui_settings_socket_file_label": "Sokkelfil",
"gui_settings_socks_label": "SOCKS-port",
"gui_settings_authenticate_label": "Valgmuligheder for Tor-autentifikation",
"gui_settings_authenticate_no_auth_option": "Ingen autentifikation, eller cookieautentifikation",
"gui_settings_authenticate_password_option": "Adgangskode",
"gui_settings_authenticate_cookie_option": "Cookie",
"gui_settings_password_label": "Adgangskode",
"gui_settings_cookie_label": "Cookiesti",
"gui_settings_tor_bridges": "Understøttelse af Tor-bro",
"gui_settings_tor_bridges_no_bridges_radio_option": "Brug ikke broer",
"gui_settings_tor_bridges_obfs4_radio_option": "Brug indbygget obfs4 udskiftelige transporter",
"gui_settings_tor_bridges_obfs4_radio_option_no_obfs4proxy": "Brug indbygget obfs4 udskiftelige transporter (kræver obfs4proxy)",
"gui_settings_tor_bridges_custom_radio_option": "Brug brugerdefinerede broer",
"gui_settings_tor_bridges_custom_label": "Du kan få broer fra <a href=\"https://bridges.torproject.org/options\">https://bridges.torproject.org</a>",
"gui_settings_tor_bridges_invalid": "Ingen af broerne du leverede ser ud til at være gyldige, så de ignoreres.\nPrøv venligst igen med gyldige broer.",
"gui_settings_button_save": "Gem",
"gui_settings_button_cancel": "Annuller",
"gui_settings_button_help": "Hjælp",
"gui_settings_shutdown_timeout_choice": "Sæt timer til automatisk stop?",
"gui_settings_shutdown_timeout": "Stop delingen ved:",
"settings_saved": "Indstillinger gemt til {}",
"settings_error_unknown": "Kan ikke oprette forbindelse til Tor-kontroller da indstillingerne ikke giver mening.",
"settings_error_automatic": "Kan ikke oprette forbindelse til Tor-kontroller. Kører Tor Browser i baggrunden? Hvis du ikke har den kan du få den fra:\nhttps://www.torproject.org/.",
"settings_error_socket_port": "Kan ikke oprette forbindelse til Tor-kontroller på {}:{}",
"settings_error_socket_file": "Kan ikke oprette forbindelse til Tor-kontroller med sokkelfilen {}",
"settings_error_auth": "Forbundet til {}:{}, men kan ikke autentificere. Er det en Tor-kontroller?",
"settings_error_missing_password": "Forbundet til Tor-kontroller, men den kræver en adgangskode for at autentificere",
"settings_error_unreadable_cookie_file": "Forbundet til Tor-kontroller, men kan ikke autentificere da din adgangskode kan være forkert, og din bruger ikke har tilladelse til at læse cookiefilen.",
"settings_error_bundled_tor_not_supported": "Bundet Tor understøttes ikke når der ikke bruges udviklertilstand i Windows eller MacOS.",
"settings_error_bundled_tor_timeout": "Det tager for længe at oprette forbindelse til Tor. Din computer er måske offline, eller dit ur går forkert.",
"settings_error_bundled_tor_canceled": "Tor-processen lukkede inden den blev færdig med at oprette forbindelse.",
"settings_error_bundled_tor_broken": "Der er noget galt med OnionShare som opretter forbindelse til Tor i baggrunden:\n{}",
"settings_test_success": "Tillykke, OnionShare kan oprette forbindelse til Tor-kontrolleren.\n\nTor version: {}\nUnderstøtter kortvarige onion-tjenester: {}\nUnderstøtter usynlige onion-tjenester: {}",
"error_tor_protocol_error": "Fejl under snak med Tor-kontrolleren.\nHvis du bruger Whonix, så tjek https://www.whonix.org/wiki/onionshare for at få OnionShare til at virke.",
"connecting_to_tor": "Forbundet til Tor-netværket",
"update_available": "Der findes en OnionShare-opdatering. <a href='{}'>Klik her</a> for at downloade den.<br><br>Installeret version: {}<br>Seneste version: {}",
"update_error_check_error": "Fejl under søgning efter opdateringer: Du er måske ikke forbundet til Tor, eller måske er OnionShare-webstedet nede.",
"update_error_invalid_latest_version": "Fejl under søgning efter opdateringer: OnionShare-webstedet svarende ved at sige at den seneste version er '{}', men det ser ikke ud til at være en gyldig versionsstreng.",
"update_not_available": "Du kører den seneste version af OnionShare.",
"gui_tor_connection_ask": "Vil du åbne OnionShare-indstillinger for at fejlsøge forbindelsen til Tor?",
"gui_tor_connection_ask_open_settings": "Åbn indstillinger",
"gui_tor_connection_ask_quit": "Afslut",
"gui_tor_connection_error_settings": "Prøv at justere måden hvorpå OnionShare opretter forbindelse til Tor-netværket i Indstillinger.",
"gui_tor_connection_canceled": "OnionShare kan ikke oprette forbindelse til Tor.\n\nSørg for at du har forbindelse til internettet, og åbn herefter OnionShare igen for at konfigurere Tor-forbindelsen.",
"gui_tor_connection_lost": "Afbryder forbindelsen fra Tor.",
"gui_server_started_after_timeout": "Serveren startede efter dit valgte automatiske timeout.\nStart venligst en ny deling.",
"gui_server_timeout_expired": "Den valgte timeout er allerede udløbet.\nOpdater venligst timeouten og herefter kan du starte deling.",
"share_via_onionshare": "Del via OnionShare",
"gui_save_private_key_checkbox": "Brug en vedvarende URL\n(fravalg vil slette gemte URL)",
"persistent_url_in_use": "Denne deling bruger en vedvarende URL"
}

View File

@ -5,17 +5,17 @@
"wait_for_hs_trying": "Trying...",
"wait_for_hs_nope": "Not ready yet.",
"wait_for_hs_yup": "Ready!",
"give_this_url": "Give this URL to the person you're sending the file to:",
"give_this_url_stealth": "Give this URL and HidServAuth line to the person you're sending the file to:",
"give_this_url": "Give this address to the person you're sending the file to:",
"give_this_url_stealth": "Give this address and HidServAuth line to the person you're sending the file to:",
"ctrlc_to_stop": "Press Ctrl-C to stop server",
"not_a_file": "{0:s} is not a valid file.",
"not_a_readable_file": "{0:s} is not a readable file.",
"no_available_port": "Could not start the Onion service as there was no available port.",
"download_page_loaded": "Download page loaded",
"other_page_loaded": "URL loaded",
"close_on_timeout": "Closing automatically because timeout was reached",
"closing_automatically": "Closing automatically because download finished",
"timeout_download_still_running": "Waiting for download to complete before auto-stopping",
"other_page_loaded": "Address loaded",
"close_on_timeout": "Stopped because timer expired",
"closing_automatically": "Stopped because download finished",
"timeout_download_still_running": "Waiting for download to complete",
"large_filesize": "Warning: Sending large files could take hours",
"error_tails_invalid_port": "Invalid value, port must be an integer",
"error_tails_unknown_root": "Unknown error with Tails root process",
@ -34,21 +34,25 @@
"help_debug": "Log application errors to stdout, and log web errors to disk",
"help_filename": "List of files or folders to share",
"help_config": "Path to a custom JSON config file (optional)",
"gui_drag_and_drop": "Drag and drop\nfiles here",
"gui_drag_and_drop": "Drag and drop files and folders\nto start sharing",
"gui_add": "Add",
"gui_delete": "Delete",
"gui_choose_items": "Choose",
"gui_start_server": "Start Sharing",
"gui_stop_server": "Stop Sharing",
"gui_copy_url": "Copy URL",
"gui_stop_server_shutdown_timeout": "Stop Sharing ({}s remaining)",
"gui_stop_server_shutdown_timeout_tooltip": "Share will stop automatically at {}",
"gui_copy_url": "Copy Address",
"gui_copy_hidservauth": "Copy HidServAuth",
"gui_downloads": "Downloads:",
"gui_canceled": "Canceled",
"gui_copied_url": "Copied URL to clipboard",
"gui_copied_hidservauth": "Copied HidServAuth line to clipboard",
"gui_copied_url_title": "Copied OnionShare address",
"gui_copied_url": "The OnionShare address has been copied to clipboard",
"gui_copied_hidservauth_title": "Copied HidServAuth",
"gui_copied_hidservauth": "The HidServAuth line has been copied to clipboard",
"gui_starting_server1": "Starting Tor onion service...",
"gui_starting_server2": "Crunching files...",
"gui_please_wait": "Please wait...",
"gui_please_wait": "Starting... Click to cancel",
"error_hs_dir_cannot_create": "Cannot create onion service dir {0:s}",
"error_hs_dir_not_writable": "onion service dir {0:s} is not writable",
"using_ephemeral": "Starting ephemeral Tor onion service and awaiting publication",
@ -56,13 +60,14 @@
"gui_download_progress_starting": "{0:s}, %p% (Computing ETA)",
"gui_download_progress_eta": "{0:s}, ETA: {1:s}, %p%",
"version_string": "Onionshare {0:s} | https://onionshare.org/",
"gui_quit_warning": "Are you sure you want to quit?\nThe URL you are sharing won't exist anymore.",
"gui_quit_title": "Transfer in Progress",
"gui_quit_warning": "You're in the process of sending files. Are you sure you want to quit OnionShare?",
"gui_quit_warning_quit": "Quit",
"gui_quit_warning_dont_quit": "Don't Quit",
"error_rate_limit": "An attacker might be trying to guess your URL. To prevent this, OnionShare has automatically stopped the server. To share the files you must start it again and share the new URL.",
"gui_quit_warning_dont_quit": "Cancel",
"error_rate_limit": "An attacker might be trying to guess your address. To prevent this, OnionShare has automatically stopped the server. To share the files you must start it again and share the new address.",
"zip_progress_bar_format": "Crunching files: %p%",
"error_stealth_not_supported": "To create stealth onion services, you need at least Tor 0.2.9.1-alpha (or Tor Browser 6.5) and at least python3-stem 1.5.0.",
"error_ephemeral_not_supported": "OnionShare requires at least at least Tor 0.2.7.1 and at least python3-stem 1.4.0.",
"error_ephemeral_not_supported": "OnionShare requires at least Tor 0.2.7.1 and at least python3-stem 1.4.0.",
"gui_settings_window_title": "Settings",
"gui_settings_stealth_label": "Stealth (advanced)",
"gui_settings_stealth_option": "Create stealth onion services",
@ -93,15 +98,19 @@
"gui_settings_cookie_label": "Cookie path",
"gui_settings_tor_bridges": "Tor Bridge support",
"gui_settings_tor_bridges_no_bridges_radio_option": "Don't use bridges",
"gui_settings_tor_bridges_obfs4_radio_option": "Use obfs4 pluggable transports",
"gui_settings_tor_bridges_obfs4_radio_option_no_obfs4proxy": "Use obfs4 pluggable transports (requires obfs4proxy)",
"gui_settings_tor_bridges_custom_radio_option": "Use custom bridges (non-pluggable transports)",
"gui_settings_tor_bridges_custom_label": "You can get bridges from <a href=\"https://bridges.torproject.org/bridges\">https://bridges.torproject.org</a>",
"gui_settings_tor_bridges_invalid": "None of the bridges you supplied seem to be valid, so they've been ignored.\nPlease try again with valid bridges.",
"gui_settings_tor_bridges_obfs4_radio_option": "Use built-in obfs4 pluggable transports",
"gui_settings_tor_bridges_obfs4_radio_option_no_obfs4proxy": "Use built-in obfs4 pluggable transports (requires obfs4proxy)",
"gui_settings_tor_bridges_meek_lite_amazon_radio_option": "Use built-in meek_lite (Amazon) pluggable transports",
"gui_settings_tor_bridges_meek_lite_amazon_radio_option_no_obfs4proxy": "Use built-in meek_lite (Amazon) pluggable transports (requires obfs4proxy)",
"gui_settings_tor_bridges_meek_lite_azure_radio_option": "Use built-in meek_lite (Azure) pluggable transports",
"gui_settings_tor_bridges_meek_lite_azure_radio_option_no_obfs4proxy": "Use built-in meek_lite (Azure) pluggable transports (requires obfs4proxy)",
"gui_settings_tor_bridges_custom_radio_option": "Use custom bridges",
"gui_settings_tor_bridges_custom_label": "You can get bridges from <a href=\"https://bridges.torproject.org/options\">https://bridges.torproject.org</a>",
"gui_settings_tor_bridges_invalid": "None of the bridges you supplied seem to be valid.\nPlease try again with valid bridges.",
"gui_settings_button_save": "Save",
"gui_settings_button_cancel": "Cancel",
"gui_settings_button_help": "Help",
"gui_settings_shutdown_timeout_choice": "Set auto-stop timer?",
"gui_settings_shutdown_timeout_checkbox": "Use auto-stop timer",
"gui_settings_shutdown_timeout": "Stop the share at:",
"settings_saved": "Settings saved to {}",
"settings_error_unknown": "Can't connect to Tor controller because the settings don't make sense.",
@ -131,6 +140,17 @@
"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.",
"share_via_onionshare": "Share via OnionShare",
"gui_save_private_key_checkbox": "Use a persistent URL\n(unchecking will delete any saved URL)",
"persistent_url_in_use": "This share is using a persistent URL"
"gui_save_private_key_checkbox": "Use a persistent address\n(unchecking will delete any saved address)",
"gui_url_description": "<b>Anyone</b> with this link can <b>download</b> your files using <b>Tor Browser</b>: <img src='{}' />",
"gui_url_label_persistent": "This share will not stop automatically unless a timer is set.<br><br>Every share will have the same address (to use one-time addresses, disable persistence in the Settings)",
"gui_url_label_stay_open": "This share will not stop automatically unless a timer is set.",
"gui_url_label_onetime": "This share will stop after the first download",
"gui_url_label_onetime_and_persistent": "This share will stop after the first download<br><br>Every share will have the same address (to use one-time addresses, disable persistence in the Settings)",
"gui_status_indicator_stopped": "Ready to Share",
"gui_status_indicator_working": "Starting...",
"gui_status_indicator_started": "Sharing",
"gui_file_info": "{} Files, {}",
"gui_file_info_single": "{} File, {}",
"info_in_progress_downloads_tooltip": "{} download(s) in progress",
"info_completed_downloads_tooltip": "{} download(s) completed"
}

View File

@ -64,7 +64,6 @@
"error_stealth_not_supported": "Om een geheime onion service te maken heb je minstens Tor 0.2.9.1-alpha (of Tor Browser 6.5) en minstens python3-stem 1.5.0 nodig.",
"error_ephemeral_not_supported": "OnionShare vereist minstens Tor 0.2.7.1 en minstens python3-stem 1.4.0.",
"gui_settings_window_title": "Instellingen",
"gui_settings_window_title": "Instellingen",
"gui_settings_stealth_label": "Stealth (geavanceerd)",
"gui_settings_stealth_option": "Maak stealth onion services",
"gui_settings_stealth_option_details": "Dit maakt OnionShare veiliger, maar ook lastiger voor de ontvanger om te verbinden.<br><a href=\"https://github.com/micahflee/onionshare/wiki/Stealth-Onion-Services\">Meer informatie</a>.",

View File

@ -0,0 +1,2 @@
Bridge meek_lite 0.0.2.0:2 B9E7141C594AF25699E0079C1F0146F409495296 url=https://d2cly7j4zqgua7.cloudfront.net/ front=a0.awsstatic.com
UseBridges 1

View File

@ -0,0 +1,2 @@
Bridge meek_lite 0.0.2.0:3 97700DFE9F483596DDA6264C4D7DF7641E1E39CE url=https://meek.azureedge.net/ front=ajax.aspnetcdn.com
UseBridges 1

View File

@ -1 +1 @@
1.2
1.3

View File

@ -1,13 +1,15 @@
import sys
# Force tests to look for resources in the source code tree
sys.onionshare_dev_mode = True
import os
import shutil
import sys
import tempfile
import pytest
from onionshare import common
@pytest.fixture
def temp_dir_1024():
""" Create a temporary directory that has a single file of a

View File

@ -193,12 +193,6 @@ class TestGetResourcePath:
common.get_resource_path(os.path.join(prefix, 'test_filename')) ==
os.path.join(prefix, 'test_filename'))
def test_frozen_windows(self, platform_windows, sys_frozen):
prefix = os.path.join(os.path.dirname(sys.executable), 'share')
assert (
common.get_resource_path(os.path.join(prefix, 'test_filename')) ==
os.path.join(prefix, 'test_filename'))
class TestGetTorPaths:
# @pytest.mark.skipif(sys.platform != 'Darwin', reason='requires MacOS') ?

View File

@ -55,11 +55,14 @@ class TestSettings:
'auth_password': '',
'close_after_first_download': True,
'systray_notifications': True,
'shutdown_timeout': False,
'use_stealth': False,
'use_autoupdate': True,
'autoupdate_timestamp': None,
'no_bridges': True,
'tor_bridges_use_obfs4': False,
'tor_bridges_use_meek_lite_amazon': False,
'tor_bridges_use_meek_lite_azure': False,
'tor_bridges_use_custom_bridges': '',
'save_private_key': False,
'private_key': '',
@ -125,6 +128,8 @@ class TestSettings:
assert settings_obj.get('autoupdate_timestamp') is None
assert settings_obj.get('no_bridges') is True
assert settings_obj.get('tor_bridges_use_obfs4') is False
assert settings_obj.get('tor_bridges_use_meek_lite_amazon') is False
assert settings_obj.get('tor_bridges_use_meek_lite_azure') is False
assert settings_obj.get('tor_bridges_use_custom_bridges') == ''