Merge branch 'develop'
1
.gitignore
vendored
@ -28,6 +28,7 @@ pip-log.txt
|
||||
.tox
|
||||
nosetests.xml
|
||||
.cache
|
||||
.pytest_cache
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
|
@ -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:
|
||||
|
2
BUILD.md
@ -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):
|
||||
|
15
CHANGELOG.md
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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()))))
|
||||
|
@ -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')
|
||||
|
@ -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;
|
||||
|
@ -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"
|
||||
|
@ -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'),
|
||||
|
@ -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
|
||||
|
@ -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'
|
||||
|
@ -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
|
||||
|
@ -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': '',
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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()
|
||||
|
@ -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):
|
||||
"""
|
||||
|
@ -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)
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
|
||||
|
@ -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()
|
||||
|
Before Width: | Height: | Size: 86 KiB After Width: | Height: | Size: 56 KiB |
Before Width: | Height: | Size: 105 KiB After Width: | Height: | Size: 117 KiB |
Before Width: | Height: | Size: 69 KiB After Width: | Height: | Size: 40 KiB |
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 45 KiB |
22
setup.py
@ -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
|
||||
)
|
||||
|
@ -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;
|
||||
|
@ -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>
|
||||
|
@ -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 }} ▼</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>
|
||||
|
BIN
share/images/download_completed.png
Normal file
After Width: | Height: | Size: 646 B |
BIN
share/images/download_completed_none.png
Normal file
After Width: | Height: | Size: 437 B |
BIN
share/images/download_in_progress.png
Normal file
After Width: | Height: | Size: 638 B |
BIN
share/images/download_in_progress_none.png
Normal file
After Width: | Height: | Size: 412 B |
Before Width: | Height: | Size: 2.0 KiB |
BIN
share/images/favicon.ico
Normal file
After Width: | Height: | Size: 4.2 KiB |
BIN
share/images/file_delete.png
Normal file
After Width: | Height: | Size: 182 B |
BIN
share/images/info.png
Normal file
After Width: | Height: | Size: 435 B |
BIN
share/images/logo_transparent.png
Normal file
After Width: | Height: | Size: 3.7 KiB |
Before Width: | Height: | Size: 346 B After Width: | Height: | Size: 347 B |
Before Width: | Height: | Size: 286 B After Width: | Height: | Size: 342 B |
Before Width: | Height: | Size: 338 B After Width: | Height: | Size: 349 B |
Before Width: | Height: | Size: 513 B |
BIN
share/images/web_file.png
Normal file
After Width: | Height: | Size: 251 B |
BIN
share/images/web_folder.png
Normal file
After Width: | Height: | Size: 338 B |
136
share/locale/da.json
Normal 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"
|
||||
}
|
@ -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"
|
||||
}
|
||||
|
@ -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>.",
|
||||
|
2
share/torrc_template-meek_lite_amazon
Normal 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
|
2
share/torrc_template-meek_lite_azure
Normal 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
|
@ -1 +1 @@
|
||||
1.2
|
||||
1.3
|
||||
|
@ -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
|
||||
|
@ -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') ?
|
||||
|
@ -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') == ''
|
||||
|
||||
|
||||
|